2022-11-16 10:29:44 +01:00
|
|
|
#!/usr/bin/env python3
|
2023-07-29 12:22:16 +02:00
|
|
|
"""Some application path."""
|
|
|
|
from typing import Any
|
2022-11-22 18:06:19 +01:00
|
|
|
import sys
|
2022-11-16 10:29:44 +01:00
|
|
|
import fastapi
|
|
|
|
|
|
|
|
from fastapi import FastAPI
|
|
|
|
from fastapi import Depends
|
|
|
|
from fastapi import Request
|
2022-11-22 18:06:19 +01:00
|
|
|
from fastapi import Response
|
|
|
|
from fastapi.exceptions import HTTPException
|
2023-07-30 14:48:50 +02:00
|
|
|
from fastapi.templating import Jinja2Templates
|
2022-11-22 18:06:19 +01:00
|
|
|
from starlette.responses import JSONResponse
|
|
|
|
|
|
|
|
from loguru import logger
|
2023-07-29 12:22:16 +02:00
|
|
|
from sqlalchemy import select
|
2022-11-22 18:06:19 +01:00
|
|
|
|
2023-07-29 12:22:16 +02:00
|
|
|
from app import models
|
2023-03-19 05:51:20 +01:00
|
|
|
from app.utils import precheck
|
2022-11-22 18:06:19 +01:00
|
|
|
from app.database import get_db_session
|
2023-03-17 10:59:29 +01:00
|
|
|
from app.config import DEBUG
|
2022-11-22 18:06:19 +01:00
|
|
|
from app.activitypub import ME
|
2023-04-02 09:14:37 +02:00
|
|
|
from app.config import BASE_URL, POST_TOKEN
|
2022-11-22 18:06:19 +01:00
|
|
|
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
|
2022-11-22 18:06:19 +01:00
|
|
|
from app.database import AsyncSession
|
2023-03-17 10:59:29 +01:00
|
|
|
from app.boxes import save_incoming
|
2023-07-29 12:22:16 +02:00
|
|
|
from app.activitypub import VisibilityEnum
|
|
|
|
from app.boxes import _send_create
|
|
|
|
from app.orgpython import to_html
|
2022-11-16 10:29:44 +01:00
|
|
|
|
2023-04-01 18:34:46 +02:00
|
|
|
|
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."""
|
2023-07-29 14:49:12 +02:00
|
|
|
if request.headers.get("Early-Data", None) == "1" \
|
|
|
|
and request.method != "GET":
|
2022-11-16 10:29:44 +01:00
|
|
|
raise fastapi.HTTPException(status_code=425, detail="Too early")
|
|
|
|
|
2023-07-29 14:49:12 +02:00
|
|
|
|
2022-11-16 10:29:44 +01:00
|
|
|
app = FastAPI(
|
2023-07-29 14:49:12 +02:00
|
|
|
docs_url=None, redoc_url=None,
|
|
|
|
dependencies=[Depends(_check_0rtt_early_data)]
|
2022-11-16 10:29:44 +01:00
|
|
|
)
|
|
|
|
|
2023-07-30 14:48:50 +02:00
|
|
|
templates = Jinja2Templates(directory="templates")
|
|
|
|
|
2022-11-22 18:06:19 +01:00
|
|
|
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")
|
2022-11-22 18:06:19 +01:00
|
|
|
|
2023-07-29 14:49:12 +02:00
|
|
|
|
|
|
|
# pylint: disable=too-few-public-methods
|
|
|
|
class ActivityPubResponse(JSONResponse):
|
2023-07-29 12:22:16 +02:00
|
|
|
"""Simple wrap JSONresponse return ActivityPub response."""
|
2023-04-01 18:34:46 +02:00
|
|
|
media_type = "application/activity+json"
|
|
|
|
|
|
|
|
|
2023-04-03 09:16:46 +02:00
|
|
|
def is_ap_requested(req: Request) -> bool:
|
2023-07-29 12:22:16 +02:00
|
|
|
"""Check resquest is ActivityPub request."""
|
2023-04-03 09:16:46 +02:00
|
|
|
accept_value = req.headers.get("accept")
|
|
|
|
if not accept_value:
|
|
|
|
return False
|
2023-07-29 12:22:16 +02:00
|
|
|
for i in [
|
2023-04-03 09:16:46 +02:00
|
|
|
"application/activity+json",
|
|
|
|
"application/ld+json",
|
2023-07-29 12:22:16 +02:00
|
|
|
]:
|
2023-04-03 09:16:46 +02:00
|
|
|
if accept_value.startswith(i):
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2022-11-16 10:29:44 +01:00
|
|
|
@app.get("/")
|
2023-04-03 09:16:46 +02:00
|
|
|
async def index(
|
2023-08-01 16:25:02 +02:00
|
|
|
request: Request,
|
|
|
|
db_session: AsyncSession = Depends(get_db_session),
|
2023-04-03 09:16:46 +02:00
|
|
|
):
|
2023-07-29 12:22:16 +02:00
|
|
|
"""Return index page."""
|
2023-04-03 09:16:46 +02:00
|
|
|
if is_ap_requested(request):
|
|
|
|
return ActivityPubResponse(ME)
|
|
|
|
|
2023-07-30 14:48:50 +02:00
|
|
|
statues = (
|
|
|
|
await db_session.scalars(
|
2023-08-01 16:25:02 +02:00
|
|
|
select(models.OutboxObject)
|
|
|
|
.where(
|
2023-07-30 14:48:50 +02:00
|
|
|
models.OutboxObject.ap_type == "Note",
|
|
|
|
models.OutboxObject.is_deleted.is_(False),
|
2023-08-01 16:25:02 +02:00
|
|
|
)
|
|
|
|
.order_by(models.OutboxObject.created_at.desc())
|
2023-07-30 14:48:50 +02:00
|
|
|
)
|
|
|
|
).all()
|
|
|
|
|
|
|
|
return templates.TemplateResponse(
|
2024-02-26 14:12:20 +01:00
|
|
|
request,
|
2023-07-30 14:48:50 +02:00
|
|
|
"index.html",
|
|
|
|
{
|
|
|
|
"request": request,
|
|
|
|
"statues": statues,
|
2023-08-01 16:25:02 +02:00
|
|
|
},
|
2023-07-30 14:48:50 +02:00
|
|
|
)
|
2022-11-22 18:06:19 +01:00
|
|
|
|
2023-07-29 12:22:16 +02:00
|
|
|
|
2022-11-22 18:06:19 +01:00
|
|
|
@app.post("/inbox")
|
|
|
|
async def inbox(
|
2023-08-01 16:25:02 +02:00
|
|
|
request: Request,
|
|
|
|
db_session: AsyncSession = Depends(get_db_session),
|
|
|
|
httpsig_checker=Depends(precheck.inbox_prechecker),
|
2022-11-22 18:06:19 +01:00
|
|
|
) -> Response:
|
2023-07-29 12:22:16 +02:00
|
|
|
"""ActivityPub inbox endpoint."""
|
2022-11-22 18:06:19 +01:00
|
|
|
payload = await request.json()
|
2023-03-17 09:49:09 +01:00
|
|
|
|
2023-07-29 12:22:16 +02:00
|
|
|
if not httpsig_checker:
|
2023-08-02 11:47:27 +02:00
|
|
|
return Response(status_code=401, content="invalid http-sig")
|
2022-11-22 18:06:19 +01:00
|
|
|
|
2023-07-29 12:22:16 +02:00
|
|
|
if not await save_incoming(db_session, payload):
|
2023-08-02 11:47:27 +02:00
|
|
|
return Response(status_code=401, content="invalid activitypub object")
|
2023-07-29 12:22:16 +02:00
|
|
|
|
|
|
|
return Response(status_code=202)
|
|
|
|
|
2023-04-01 18:34:46 +02:00
|
|
|
|
2023-04-02 09:14:37 +02:00
|
|
|
@app.post("/outbox")
|
|
|
|
async def outbox(
|
|
|
|
request: Request,
|
|
|
|
db_session: AsyncSession = Depends(get_db_session),
|
|
|
|
) -> Response:
|
2023-07-29 12:22:16 +02:00
|
|
|
"""ActivityPub outbox endpoint, now only process client post request."""
|
2024-03-04 13:57:57 +01:00
|
|
|
payload = await request.json()
|
|
|
|
logger.info(payload)
|
2023-04-02 09:48:17 +02:00
|
|
|
post_token = request.headers.get("Authorization")
|
2023-04-02 09:14:37 +02:00
|
|
|
|
2023-07-29 14:49:12 +02:00
|
|
|
def _check_post_token() -> bool:
|
|
|
|
if post_token is None:
|
|
|
|
return False
|
|
|
|
if POST_TOKEN != post_token.split(" ")[1]:
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
if not _check_post_token():
|
2023-07-29 12:22:16 +02:00
|
|
|
logger.warning("Non-valid post token!")
|
|
|
|
return Response(status_code=406)
|
2023-04-02 09:14:37 +02:00
|
|
|
|
2023-07-29 12:22:16 +02:00
|
|
|
logger.info("True token")
|
2024-03-04 13:57:57 +01:00
|
|
|
note_content = to_html(payload["content"]).replace("\n", "")
|
2023-04-02 09:14:37 +02:00
|
|
|
|
2023-07-29 12:22:16 +02:00
|
|
|
await _send_create(
|
|
|
|
db_session,
|
|
|
|
"Note",
|
2024-03-04 13:57:57 +01:00
|
|
|
note_content,
|
|
|
|
payload["visibility"]
|
2023-07-29 12:22:16 +02:00
|
|
|
)
|
|
|
|
return Response(status_code=200)
|
2023-04-02 09:14:37 +02:00
|
|
|
|
|
|
|
|
2023-04-01 18:34:46 +02:00
|
|
|
@app.get("/tail/{public_id}")
|
|
|
|
async def outbox_activity_by_public_id(
|
|
|
|
public_id: str,
|
|
|
|
db_session: AsyncSession = Depends(get_db_session),
|
|
|
|
) -> ActivityPubResponse:
|
2023-07-29 12:22:16 +02:00
|
|
|
"""Return note page."""
|
2023-04-01 18:34:46 +02:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
2022-11-22 18:06:19 +01:00
|
|
|
@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)
|
|
|
|
|
2023-08-01 16:25:02 +02:00
|
|
|
response = {
|
2022-11-22 18:06:19 +01:00
|
|
|
"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(
|
2023-08-01 16:25:02 +02:00
|
|
|
response,
|
2022-11-22 18:06:19 +01:00
|
|
|
media_type="application/jrd+json; charset=utf-8",
|
|
|
|
headers={"Access-Control-Allow-Origin": "*"},
|
|
|
|
)
|
2023-03-18 05:56:54 +01:00
|
|
|
|
2023-07-29 12:22:16 +02:00
|
|
|
|
2023-03-18 05:56:54 +01:00
|
|
|
@app.get("/.well-known/nodeinfo")
|
|
|
|
async def well_known_nodeinfo() -> dict[str, Any]:
|
2023-07-29 12:22:16 +02:00
|
|
|
"""Exposes nodeinfo path."""
|
2023-03-18 05:56:54 +01:00
|
|
|
return {
|
|
|
|
"links": [
|
|
|
|
{
|
|
|
|
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
|
|
|
"href": f"{BASE_URL}/nodeinfo/2.0",
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/nodeinfo/2.0")
|
2023-07-29 12:22:16 +02:00
|
|
|
async def nodeinfo():
|
|
|
|
"""Return site nodeinfo."""
|
2023-03-18 05:56:54 +01:00
|
|
|
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": []},
|
2023-07-29 14:49:12 +02:00
|
|
|
"usage": {"users": {"total": 1}, "localPosts": 0}, # TODO
|
2023-03-18 05:56:54 +01:00
|
|
|
"openRegistrations": False,
|
|
|
|
"metadata": {},
|
|
|
|
},
|
|
|
|
)
|