foxhole/app/main.py

184 lines
5 KiB
Python
Raw Normal View History

2022-11-16 10:29:44 +01:00
#!/usr/bin/env python3
2023-03-17 09:49:09 +01:00
import logging
import sys
2022-11-16 10:29:44 +01:00
import fastapi
2023-03-17 09:49:09 +01:00
import httpx
import json
2022-11-16 10:29:44 +01:00
from fastapi import FastAPI
from fastapi import Depends
from fastapi import Request
from fastapi import Response
from fastapi.exceptions import HTTPException
from sqlalchemy.util import monkeypatch_proxied_specials
from starlette.responses import JSONResponse
2023-03-18 05:56:54 +01:00
from typing import Any
from loguru import logger
from app.utils import precheck
from app.database import get_db_session
2023-03-17 10:59:29 +01:00
from app.config import DEBUG
from app.activitypub import ME
2023-04-02 09:14:37 +02:00
from app.config import BASE_URL, POST_TOKEN
from app.config import DOMAIN
from app.config import ID
from app.config import USERNAME
2023-03-18 06:08:17 +01:00
from app.config import VERSION
from app.database import AsyncSession
from app.database import get_db_session
2023-03-17 10:59:29 +01:00
from app.boxes import save_incoming
2022-11-16 10:29:44 +01:00
from sqlalchemy import select
from app import models
2022-11-16 10:29:44 +01:00
def _check_0rtt_early_data(request: Request) -> None:
"""Disable TLS1.3 0-RTT requests for non-GET."""
if request.headers.get("Early-Data", None) == "1" and request.method != "GET":
raise fastapi.HTTPException(status_code=425, detail="Too early")
app = FastAPI(
docs_url=None, redoc_url=None, dependencies=[Depends(_check_0rtt_early_data)]
)
logger.remove()
logger.add(sys.stdout, level="DEBUG" if DEBUG else "INFO")
2023-03-18 15:39:46 +01:00
logger.add("output.log", level="DEBUG")
class ActivityPubResponse(JSONResponse):
media_type = "application/activity+json"
2022-11-16 10:29:44 +01:00
@app.get("/")
async def index():
return ME
@app.post("/inbox")
async def inbox(
request: Request,
db_session: AsyncSession = Depends(get_db_session),
httpsig_checker = Depends(precheck.inbox_prechecker),
) -> Response:
payload = await request.json()
2023-03-17 09:49:09 +01:00
2023-03-17 10:59:29 +01:00
if httpsig_checker:
await save_incoming(db_session, payload)
return Response(status_code=202)
else:
return Response(status_code=406)
2023-04-02 09:14:37 +02:00
@app.post("/outbox")
async def outbox(
request: Request,
db_session: AsyncSession = Depends(get_db_session),
) -> Response:
payload = await request.body()
2023-04-02 09:48:17 +02:00
content = payload.decode("utf-8")
logger.info(content)
post_token = request.headers.get("Authorization")
2023-04-02 09:14:37 +02:00
if POST_TOKEN is not None \
2023-04-02 09:48:17 +02:00
and POST_TOKEN == post_token.split(" ")[1]: # type: ignore
2023-04-02 09:14:37 +02:00
logger.info("True token")
from app.activitypub import VisibilityEnum
from app.boxes import _send_create
from app.orgpython import to_html
content = to_html(content).replace("\n", "")
await _send_create(
db_session,
"Note",
content,
VisibilityEnum.PUBLIC
)
return Response(status_code=200)
else:
logger.warning("Non-valid post token!")
return Response(status_code=406)
@app.get("/tail/{public_id}")
async def outbox_activity_by_public_id(
public_id: str,
request: Request,
db_session: AsyncSession = Depends(get_db_session),
) -> ActivityPubResponse:
outbox_object = (
await db_session.execute(
select(models.OutboxObject).where(
models.OutboxObject.public_id == public_id,
models.OutboxObject.is_deleted.is_(False),
)
)
).scalar_one_or_none()
if not outbox_object:
raise HTTPException(status_code=404)
return ActivityPubResponse(outbox_object.ap_object)
@app.get("/.well-known/webfinger")
async def wellknown_webfinger(resource: str) -> JSONResponse:
"""Exposes/servers WebFinger data."""
if resource not in [f"acct:{USERNAME}@{DOMAIN}", ID]:
logger.info(f"Got invalid req for {resource}")
raise HTTPException(status_code=404)
out = {
"subject": f"acct:{USERNAME}@{DOMAIN}",
"aliases": [ID],
"links": [
{
"rel": "http://webfinger.net/rel/profile-page",
"type": "text/html",
"href": ID + "/",
},
{"rel": "self", "type": "application/activity+json", "href": ID},
{
"rel": "http://ostatus.org/schema/1.0/subscribe",
"template": BASE_URL + "/admin/lookup?query={uri}",
},
],
}
return JSONResponse(
out,
media_type="application/jrd+json; charset=utf-8",
headers={"Access-Control-Allow-Origin": "*"},
)
2023-03-18 05:56:54 +01:00
@app.get("/.well-known/nodeinfo")
async def well_known_nodeinfo() -> dict[str, Any]:
return {
"links": [
{
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
"href": f"{BASE_URL}/nodeinfo/2.0",
}
]
}
@app.get("/nodeinfo/2.0")
async def nodeinfo(
db_session: AsyncSession = Depends(get_db_session),
):
return JSONResponse(
{
"version": "2.0",
"software": {
"name": "foxhole",
2023-03-18 06:08:17 +01:00
"version": VERSION,
2023-03-18 05:56:54 +01:00
},
"protocols": ["activitypub"],
"services": {"inbound": [], "outbound": []},
"usage": {"users": {"total": 1}, "localPosts": 0}, #TODO
"openRegistrations": False,
"metadata": {},
},
)