forked from forks/microblog.pub
Switch to aiosqlite
This commit is contained in:
parent
18bd2cb664
commit
1f54a6a6ac
21 changed files with 698 additions and 549 deletions
32
app/actor.py
32
app/actor.py
|
@ -4,11 +4,11 @@ from typing import Union
|
|||
from urllib.parse import urlparse
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from app import activitypub as ap
|
||||
from app import media
|
||||
from app.database import AsyncSession
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from app.models import Actor as ActorModel
|
||||
|
@ -131,7 +131,7 @@ class RemoteActor(Actor):
|
|||
LOCAL_ACTOR = RemoteActor(ap_actor=ap.ME)
|
||||
|
||||
|
||||
def save_actor(db: Session, ap_actor: ap.RawObject) -> "ActorModel":
|
||||
async def save_actor(db_session: AsyncSession, ap_actor: ap.RawObject) -> "ActorModel":
|
||||
from app import models
|
||||
|
||||
if ap_type := ap_actor.get("type") not in ap.ACTOR_TYPES:
|
||||
|
@ -143,23 +143,25 @@ def save_actor(db: Session, ap_actor: ap.RawObject) -> "ActorModel":
|
|||
ap_type=ap_actor["type"],
|
||||
handle=_handle(ap_actor),
|
||||
)
|
||||
db.add(actor)
|
||||
db.commit()
|
||||
db.refresh(actor)
|
||||
db_session.add(actor)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(actor)
|
||||
return actor
|
||||
|
||||
|
||||
def fetch_actor(db: Session, actor_id: str) -> "ActorModel":
|
||||
async def fetch_actor(db_session: AsyncSession, actor_id: str) -> "ActorModel":
|
||||
from app import models
|
||||
|
||||
existing_actor = db.execute(
|
||||
existing_actor = (
|
||||
await db_session.scalars(
|
||||
select(models.Actor).where(models.Actor.ap_id == actor_id)
|
||||
).scalar_one_or_none()
|
||||
)
|
||||
).one_or_none()
|
||||
if existing_actor:
|
||||
return existing_actor
|
||||
|
||||
ap_actor = ap.get(actor_id)
|
||||
return save_actor(db, ap_actor)
|
||||
return await save_actor(db_session, ap_actor)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -175,8 +177,8 @@ class ActorMetadata:
|
|||
ActorsMetadata = dict[str, ActorMetadata]
|
||||
|
||||
|
||||
def get_actors_metadata(
|
||||
db: Session,
|
||||
async def get_actors_metadata(
|
||||
db_session: AsyncSession,
|
||||
actors: list[Union["ActorModel", "RemoteActor"]],
|
||||
) -> ActorsMetadata:
|
||||
from app import models
|
||||
|
@ -184,17 +186,19 @@ def get_actors_metadata(
|
|||
ap_actor_ids = [actor.ap_id for actor in actors]
|
||||
followers = {
|
||||
follower.ap_actor_id: follower.inbox_object.ap_id
|
||||
for follower in db.scalars(
|
||||
for follower in (
|
||||
await db_session.scalars(
|
||||
select(models.Follower)
|
||||
.where(models.Follower.ap_actor_id.in_(ap_actor_ids))
|
||||
.options(joinedload(models.Follower.inbox_object))
|
||||
)
|
||||
)
|
||||
.unique()
|
||||
.all()
|
||||
}
|
||||
following = {
|
||||
following.ap_actor_id
|
||||
for following in db.execute(
|
||||
for following in await db_session.execute(
|
||||
select(models.Following.ap_actor_id).where(
|
||||
models.Following.ap_actor_id.in_(ap_actor_ids)
|
||||
)
|
||||
|
@ -202,7 +206,7 @@ def get_actors_metadata(
|
|||
}
|
||||
sent_follow_requests = {
|
||||
follow_req.ap_object["object"]: follow_req.ap_id
|
||||
for follow_req in db.execute(
|
||||
for follow_req in await db_session.execute(
|
||||
select(models.OutboxObject.ap_object, models.OutboxObject.ap_id).where(
|
||||
models.OutboxObject.ap_type == "Follow",
|
||||
models.OutboxObject.undone_by_outbox_object_id.is_(None),
|
||||
|
|
219
app/admin.py
219
app/admin.py
|
@ -8,7 +8,6 @@ from fastapi.exceptions import HTTPException
|
|||
from fastapi.responses import RedirectResponse
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from app import activitypub as ap
|
||||
|
@ -25,7 +24,8 @@ from app.config import generate_csrf_token
|
|||
from app.config import session_serializer
|
||||
from app.config import verify_csrf_token
|
||||
from app.config import verify_password
|
||||
from app.database import get_db
|
||||
from app.database import AsyncSession
|
||||
from app.database import get_db_session
|
||||
from app.lookup import lookup
|
||||
from app.uploads import save_upload
|
||||
from app.utils import pagination
|
||||
|
@ -62,30 +62,36 @@ unauthenticated_router = APIRouter()
|
|||
|
||||
|
||||
@router.get("/")
|
||||
def admin_index(
|
||||
async def admin_index(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> templates.TemplateResponse:
|
||||
return templates.render_template(db, request, "index.html", {"request": request})
|
||||
return await templates.render_template(
|
||||
db_session, request, "index.html", {"request": request}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/lookup")
|
||||
def get_lookup(
|
||||
async def get_lookup(
|
||||
request: Request,
|
||||
query: str | None = None,
|
||||
db: Session = Depends(get_db),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> templates.TemplateResponse:
|
||||
ap_object = None
|
||||
actors_metadata = {}
|
||||
if query:
|
||||
ap_object = lookup(db, query)
|
||||
ap_object = await lookup(db_session, query)
|
||||
if ap_object.ap_type in ap.ACTOR_TYPES:
|
||||
actors_metadata = get_actors_metadata(db, [ap_object]) # type: ignore
|
||||
actors_metadata = await get_actors_metadata(
|
||||
db_session, [ap_object] # type: ignore
|
||||
)
|
||||
else:
|
||||
actors_metadata = get_actors_metadata(db, [ap_object.actor]) # type: ignore
|
||||
actors_metadata = await get_actors_metadata(
|
||||
db_session, [ap_object.actor] # type: ignore
|
||||
)
|
||||
print(ap_object)
|
||||
return templates.render_template(
|
||||
db,
|
||||
return await templates.render_template(
|
||||
db_session,
|
||||
request,
|
||||
"lookup.html",
|
||||
{
|
||||
|
@ -97,16 +103,18 @@ def get_lookup(
|
|||
|
||||
|
||||
@router.get("/new")
|
||||
def admin_new(
|
||||
async def admin_new(
|
||||
request: Request,
|
||||
query: str | None = None,
|
||||
in_reply_to: str | None = None,
|
||||
db: Session = Depends(get_db),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> templates.TemplateResponse:
|
||||
content = ""
|
||||
in_reply_to_object = None
|
||||
if in_reply_to:
|
||||
in_reply_to_object = boxes.get_anybox_object_by_ap_id(db, in_reply_to)
|
||||
in_reply_to_object = await boxes.get_anybox_object_by_ap_id(
|
||||
db_session, in_reply_to
|
||||
)
|
||||
|
||||
# Add mentions to the initial note content
|
||||
if not in_reply_to_object:
|
||||
|
@ -117,8 +125,8 @@ def admin_new(
|
|||
if tag.get("type") == "Mention" and tag["name"] != LOCAL_ACTOR.handle:
|
||||
content += f'{tag["name"]} '
|
||||
|
||||
return templates.render_template(
|
||||
db,
|
||||
return await templates.render_template(
|
||||
db_session,
|
||||
request,
|
||||
"admin_new.html",
|
||||
{
|
||||
|
@ -138,12 +146,13 @@ def admin_new(
|
|||
|
||||
|
||||
@router.get("/bookmarks")
|
||||
def admin_bookmarks(
|
||||
async def admin_bookmarks(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> templates.TemplateResponse:
|
||||
stream = (
|
||||
db.scalars(
|
||||
(
|
||||
await db_session.scalars(
|
||||
select(models.InboxObject)
|
||||
.where(
|
||||
models.InboxObject.ap_type.in_(
|
||||
|
@ -155,11 +164,12 @@ def admin_bookmarks(
|
|||
)
|
||||
.order_by(models.InboxObject.ap_published_at.desc())
|
||||
.limit(20)
|
||||
)
|
||||
).all()
|
||||
# TODO: joinedload + unique
|
||||
)
|
||||
return templates.render_template(
|
||||
db,
|
||||
return await templates.render_template(
|
||||
db_session,
|
||||
request,
|
||||
"admin_stream.html",
|
||||
{
|
||||
|
@ -169,9 +179,9 @@ def admin_bookmarks(
|
|||
|
||||
|
||||
@router.get("/inbox")
|
||||
def admin_inbox(
|
||||
async def admin_inbox(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
filter_by: str | None = None,
|
||||
cursor: str | None = None,
|
||||
) -> templates.TemplateResponse:
|
||||
|
@ -184,18 +194,23 @@ def admin_inbox(
|
|||
)
|
||||
|
||||
page_size = 20
|
||||
remaining_count = db.scalar(select(func.count(models.InboxObject.id)).where(*where))
|
||||
remaining_count = await db_session.scalar(
|
||||
select(func.count(models.InboxObject.id)).where(*where)
|
||||
)
|
||||
q = select(models.InboxObject).where(*where)
|
||||
|
||||
inbox = (
|
||||
db.scalars(
|
||||
(
|
||||
await db_session.scalars(
|
||||
q.options(
|
||||
joinedload(models.InboxObject.relates_to_inbox_object),
|
||||
joinedload(models.InboxObject.relates_to_outbox_object),
|
||||
joinedload(models.InboxObject.actor),
|
||||
)
|
||||
.order_by(models.InboxObject.ap_published_at.desc())
|
||||
.limit(20)
|
||||
)
|
||||
)
|
||||
.unique()
|
||||
.all()
|
||||
)
|
||||
|
@ -206,8 +221,8 @@ def admin_inbox(
|
|||
else None
|
||||
)
|
||||
|
||||
actors_metadata = get_actors_metadata(
|
||||
db,
|
||||
actors_metadata = await get_actors_metadata(
|
||||
db_session,
|
||||
[
|
||||
inbox_object.actor
|
||||
for inbox_object in inbox
|
||||
|
@ -215,8 +230,8 @@ def admin_inbox(
|
|||
],
|
||||
)
|
||||
|
||||
return templates.render_template(
|
||||
db,
|
||||
return await templates.render_template(
|
||||
db_session,
|
||||
request,
|
||||
"admin_inbox.html",
|
||||
{
|
||||
|
@ -228,9 +243,9 @@ def admin_inbox(
|
|||
|
||||
|
||||
@router.get("/outbox")
|
||||
def admin_outbox(
|
||||
async def admin_outbox(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
filter_by: str | None = None,
|
||||
cursor: str | None = None,
|
||||
) -> templates.TemplateResponse:
|
||||
|
@ -243,13 +258,14 @@ def admin_outbox(
|
|||
)
|
||||
|
||||
page_size = 20
|
||||
remaining_count = db.scalar(
|
||||
remaining_count = await db_session.scalar(
|
||||
select(func.count(models.OutboxObject.id)).where(*where)
|
||||
)
|
||||
q = select(models.OutboxObject).where(*where)
|
||||
|
||||
outbox = (
|
||||
db.scalars(
|
||||
(
|
||||
await db_session.scalars(
|
||||
q.options(
|
||||
joinedload(models.OutboxObject.relates_to_inbox_object),
|
||||
joinedload(models.OutboxObject.relates_to_outbox_object),
|
||||
|
@ -258,6 +274,7 @@ def admin_outbox(
|
|||
.order_by(models.OutboxObject.ap_published_at.desc())
|
||||
.limit(page_size)
|
||||
)
|
||||
)
|
||||
.unique()
|
||||
.all()
|
||||
)
|
||||
|
@ -268,8 +285,8 @@ def admin_outbox(
|
|||
else None
|
||||
)
|
||||
|
||||
actors_metadata = get_actors_metadata(
|
||||
db,
|
||||
actors_metadata = await get_actors_metadata(
|
||||
db_session,
|
||||
[
|
||||
outbox_object.relates_to_actor
|
||||
for outbox_object in outbox
|
||||
|
@ -277,8 +294,8 @@ def admin_outbox(
|
|||
],
|
||||
)
|
||||
|
||||
return templates.render_template(
|
||||
db,
|
||||
return await templates.render_template(
|
||||
db_session,
|
||||
request,
|
||||
"admin_outbox.html",
|
||||
{
|
||||
|
@ -290,11 +307,12 @@ def admin_outbox(
|
|||
|
||||
|
||||
@router.get("/notifications")
|
||||
def get_notifications(
|
||||
request: Request, db: Session = Depends(get_db)
|
||||
async def get_notifications(
|
||||
request: Request, db_session: AsyncSession = Depends(get_db_session)
|
||||
) -> templates.TemplateResponse:
|
||||
notifications = (
|
||||
db.scalars(
|
||||
(
|
||||
await db_session.scalars(
|
||||
select(models.Notification)
|
||||
.options(
|
||||
joinedload(models.Notification.actor),
|
||||
|
@ -303,19 +321,20 @@ def get_notifications(
|
|||
)
|
||||
.order_by(models.Notification.created_at.desc())
|
||||
)
|
||||
)
|
||||
.unique()
|
||||
.all()
|
||||
)
|
||||
actors_metadata = get_actors_metadata(
|
||||
db, [notif.actor for notif in notifications if notif.actor]
|
||||
actors_metadata = await get_actors_metadata(
|
||||
db_session, [notif.actor for notif in notifications if notif.actor]
|
||||
)
|
||||
|
||||
for notif in notifications:
|
||||
notif.is_new = False
|
||||
db.commit()
|
||||
await db_session.commit()
|
||||
|
||||
return templates.render_template(
|
||||
db,
|
||||
return await templates.render_template(
|
||||
db_session,
|
||||
request,
|
||||
"notifications.html",
|
||||
{
|
||||
|
@ -326,19 +345,19 @@ def get_notifications(
|
|||
|
||||
|
||||
@router.get("/object")
|
||||
def admin_object(
|
||||
async def admin_object(
|
||||
request: Request,
|
||||
ap_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> templates.TemplateResponse:
|
||||
requested_object = boxes.get_anybox_object_by_ap_id(db, ap_id)
|
||||
requested_object = await boxes.get_anybox_object_by_ap_id(db_session, ap_id)
|
||||
if not requested_object:
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
replies_tree = boxes.get_replies_tree(db, requested_object)
|
||||
replies_tree = await boxes.get_replies_tree(db_session, requested_object)
|
||||
|
||||
return templates.render_template(
|
||||
db,
|
||||
return await templates.render_template(
|
||||
db_session,
|
||||
request,
|
||||
"object.html",
|
||||
{"replies_tree": replies_tree},
|
||||
|
@ -346,30 +365,34 @@ def admin_object(
|
|||
|
||||
|
||||
@router.get("/profile")
|
||||
def admin_profile(
|
||||
async def admin_profile(
|
||||
request: Request,
|
||||
actor_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> templates.TemplateResponse:
|
||||
actor = db.execute(
|
||||
actor = (
|
||||
await db_session.execute(
|
||||
select(models.Actor).where(models.Actor.ap_id == actor_id)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if not actor:
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
actors_metadata = get_actors_metadata(db, [actor])
|
||||
actors_metadata = await get_actors_metadata(db_session, [actor])
|
||||
|
||||
inbox_objects = db.scalars(
|
||||
inbox_objects = (
|
||||
await db_session.scalars(
|
||||
select(models.InboxObject)
|
||||
.where(
|
||||
models.InboxObject.actor_id == actor.id,
|
||||
models.InboxObject.ap_type.in_(["Note", "Article", "Video"]),
|
||||
)
|
||||
.order_by(models.InboxObject.ap_published_at.desc())
|
||||
)
|
||||
).all()
|
||||
|
||||
return templates.render_template(
|
||||
db,
|
||||
return await templates.render_template(
|
||||
db_session,
|
||||
request,
|
||||
"admin_profile.html",
|
||||
{
|
||||
|
@ -381,120 +404,120 @@ def admin_profile(
|
|||
|
||||
|
||||
@router.post("/actions/follow")
|
||||
def admin_actions_follow(
|
||||
async def admin_actions_follow(
|
||||
request: Request,
|
||||
ap_actor_id: str = Form(),
|
||||
redirect_url: str = Form(),
|
||||
csrf_check: None = Depends(verify_csrf_token),
|
||||
db: Session = Depends(get_db),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> RedirectResponse:
|
||||
print(f"Following {ap_actor_id}")
|
||||
send_follow(db, ap_actor_id)
|
||||
await send_follow(db_session, ap_actor_id)
|
||||
return RedirectResponse(redirect_url, status_code=302)
|
||||
|
||||
|
||||
@router.post("/actions/like")
|
||||
def admin_actions_like(
|
||||
async def admin_actions_like(
|
||||
request: Request,
|
||||
ap_object_id: str = Form(),
|
||||
redirect_url: str = Form(),
|
||||
csrf_check: None = Depends(verify_csrf_token),
|
||||
db: Session = Depends(get_db),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> RedirectResponse:
|
||||
boxes.send_like(db, ap_object_id)
|
||||
await boxes.send_like(db_session, ap_object_id)
|
||||
return RedirectResponse(redirect_url, status_code=302)
|
||||
|
||||
|
||||
@router.post("/actions/undo")
|
||||
def admin_actions_undo(
|
||||
async def admin_actions_undo(
|
||||
request: Request,
|
||||
ap_object_id: str = Form(),
|
||||
redirect_url: str = Form(),
|
||||
csrf_check: None = Depends(verify_csrf_token),
|
||||
db: Session = Depends(get_db),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> RedirectResponse:
|
||||
boxes.send_undo(db, ap_object_id)
|
||||
await boxes.send_undo(db_session, ap_object_id)
|
||||
return RedirectResponse(redirect_url, status_code=302)
|
||||
|
||||
|
||||
@router.post("/actions/announce")
|
||||
def admin_actions_announce(
|
||||
async def admin_actions_announce(
|
||||
request: Request,
|
||||
ap_object_id: str = Form(),
|
||||
redirect_url: str = Form(),
|
||||
csrf_check: None = Depends(verify_csrf_token),
|
||||
db: Session = Depends(get_db),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> RedirectResponse:
|
||||
boxes.send_announce(db, ap_object_id)
|
||||
await boxes.send_announce(db_session, ap_object_id)
|
||||
return RedirectResponse(redirect_url, status_code=302)
|
||||
|
||||
|
||||
@router.post("/actions/bookmark")
|
||||
def admin_actions_bookmark(
|
||||
async def admin_actions_bookmark(
|
||||
request: Request,
|
||||
ap_object_id: str = Form(),
|
||||
redirect_url: str = Form(),
|
||||
csrf_check: None = Depends(verify_csrf_token),
|
||||
db: Session = Depends(get_db),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> RedirectResponse:
|
||||
inbox_object = get_inbox_object_by_ap_id(db, ap_object_id)
|
||||
inbox_object = await get_inbox_object_by_ap_id(db_session, ap_object_id)
|
||||
if not inbox_object:
|
||||
raise ValueError("Should never happen")
|
||||
inbox_object.is_bookmarked = True
|
||||
db.commit()
|
||||
await db_session.commit()
|
||||
return RedirectResponse(redirect_url, status_code=302)
|
||||
|
||||
|
||||
@router.post("/actions/unbookmark")
|
||||
def admin_actions_unbookmark(
|
||||
async def admin_actions_unbookmark(
|
||||
request: Request,
|
||||
ap_object_id: str = Form(),
|
||||
redirect_url: str = Form(),
|
||||
csrf_check: None = Depends(verify_csrf_token),
|
||||
db: Session = Depends(get_db),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> RedirectResponse:
|
||||
inbox_object = get_inbox_object_by_ap_id(db, ap_object_id)
|
||||
inbox_object = await get_inbox_object_by_ap_id(db_session, ap_object_id)
|
||||
if not inbox_object:
|
||||
raise ValueError("Should never happen")
|
||||
inbox_object.is_bookmarked = False
|
||||
db.commit()
|
||||
await db_session.commit()
|
||||
return RedirectResponse(redirect_url, status_code=302)
|
||||
|
||||
|
||||
@router.post("/actions/pin")
|
||||
def admin_actions_pin(
|
||||
async def admin_actions_pin(
|
||||
request: Request,
|
||||
ap_object_id: str = Form(),
|
||||
redirect_url: str = Form(),
|
||||
csrf_check: None = Depends(verify_csrf_token),
|
||||
db: Session = Depends(get_db),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> RedirectResponse:
|
||||
outbox_object = get_outbox_object_by_ap_id(db, ap_object_id)
|
||||
outbox_object = await get_outbox_object_by_ap_id(db_session, ap_object_id)
|
||||
if not outbox_object:
|
||||
raise ValueError("Should never happen")
|
||||
outbox_object.is_pinned = True
|
||||
db.commit()
|
||||
await db_session.commit()
|
||||
return RedirectResponse(redirect_url, status_code=302)
|
||||
|
||||
|
||||
@router.post("/actions/unpin")
|
||||
def admin_actions_unpin(
|
||||
async def admin_actions_unpin(
|
||||
request: Request,
|
||||
ap_object_id: str = Form(),
|
||||
redirect_url: str = Form(),
|
||||
csrf_check: None = Depends(verify_csrf_token),
|
||||
db: Session = Depends(get_db),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> RedirectResponse:
|
||||
outbox_object = get_outbox_object_by_ap_id(db, ap_object_id)
|
||||
outbox_object = await get_outbox_object_by_ap_id(db_session, ap_object_id)
|
||||
if not outbox_object:
|
||||
raise ValueError("Should never happen")
|
||||
outbox_object.is_pinned = False
|
||||
db.commit()
|
||||
await db_session.commit()
|
||||
return RedirectResponse(redirect_url, status_code=302)
|
||||
|
||||
|
||||
@router.post("/actions/new")
|
||||
def admin_actions_new(
|
||||
async def admin_actions_new(
|
||||
request: Request,
|
||||
files: list[UploadFile] = [],
|
||||
content: str = Form(),
|
||||
|
@ -504,16 +527,16 @@ def admin_actions_new(
|
|||
is_sensitive: bool = Form(False),
|
||||
visibility: str = Form(),
|
||||
csrf_check: None = Depends(verify_csrf_token),
|
||||
db: Session = Depends(get_db),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> RedirectResponse:
|
||||
# XXX: for some reason, no files restuls in an empty single file
|
||||
uploads = []
|
||||
if len(files) >= 1 and files[0].filename:
|
||||
for f in files:
|
||||
upload = save_upload(db, f)
|
||||
upload = await save_upload(db_session, f)
|
||||
uploads.append((upload, f.filename))
|
||||
public_id = boxes.send_create(
|
||||
db,
|
||||
public_id = await boxes.send_create(
|
||||
db_session,
|
||||
source=content,
|
||||
uploads=uploads,
|
||||
in_reply_to=in_reply_to or None,
|
||||
|
@ -528,12 +551,12 @@ def admin_actions_new(
|
|||
|
||||
|
||||
@unauthenticated_router.get("/login")
|
||||
def login(
|
||||
async def login(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> templates.TemplateResponse:
|
||||
return templates.render_template(
|
||||
db,
|
||||
return await templates.render_template(
|
||||
db_session,
|
||||
request,
|
||||
"login.html",
|
||||
{"csrf_token": generate_csrf_token()},
|
||||
|
@ -541,7 +564,7 @@ def login(
|
|||
|
||||
|
||||
@unauthenticated_router.post("/login")
|
||||
def login_validation(
|
||||
async def login_validation(
|
||||
request: Request,
|
||||
password: str = Form(),
|
||||
csrf_check: None = Depends(verify_csrf_token),
|
||||
|
@ -556,7 +579,7 @@ def login_validation(
|
|||
|
||||
|
||||
@router.get("/logout")
|
||||
def logout(
|
||||
async def logout(
|
||||
request: Request,
|
||||
) -> RedirectResponse:
|
||||
resp = RedirectResponse(request.url_for("index"), status_code=302)
|
||||
|
|
282
app/boxes.py
282
app/boxes.py
|
@ -12,7 +12,6 @@ from sqlalchemy import func
|
|||
from sqlalchemy import select
|
||||
from sqlalchemy import update
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from app import activitypub as ap
|
||||
|
@ -26,6 +25,7 @@ from app.actor import save_actor
|
|||
from app.ap_object import RemoteObject
|
||||
from app.config import BASE_URL
|
||||
from app.config import ID
|
||||
from app.database import AsyncSession
|
||||
from app.database import now
|
||||
from app.outgoing_activities import new_outgoing_activity
|
||||
from app.source import markdownify
|
||||
|
@ -42,8 +42,8 @@ def outbox_object_id(outbox_id) -> str:
|
|||
return f"{BASE_URL}/o/{outbox_id}"
|
||||
|
||||
|
||||
def save_outbox_object(
|
||||
db: Session,
|
||||
async def save_outbox_object(
|
||||
db_session: AsyncSession,
|
||||
public_id: str,
|
||||
raw_object: ap.RawObject,
|
||||
relates_to_inbox_object_id: int | None = None,
|
||||
|
@ -68,15 +68,15 @@ def save_outbox_object(
|
|||
is_hidden_from_homepage=True if ra.in_reply_to else False,
|
||||
source=source,
|
||||
)
|
||||
db.add(outbox_object)
|
||||
db.commit()
|
||||
db.refresh(outbox_object)
|
||||
db_session.add(outbox_object)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(outbox_object)
|
||||
|
||||
return outbox_object
|
||||
|
||||
|
||||
def send_like(db: Session, ap_object_id: str) -> None:
|
||||
inbox_object = get_inbox_object_by_ap_id(db, ap_object_id)
|
||||
async def send_like(db_session: AsyncSession, ap_object_id: str) -> None:
|
||||
inbox_object = await get_inbox_object_by_ap_id(db_session, ap_object_id)
|
||||
if not inbox_object:
|
||||
raise ValueError(f"{ap_object_id} not found in the inbox")
|
||||
|
||||
|
@ -88,20 +88,22 @@ def send_like(db: Session, ap_object_id: str) -> None:
|
|||
"actor": ID,
|
||||
"object": ap_object_id,
|
||||
}
|
||||
outbox_object = save_outbox_object(
|
||||
db, like_id, like, relates_to_inbox_object_id=inbox_object.id
|
||||
outbox_object = await save_outbox_object(
|
||||
db_session, like_id, like, relates_to_inbox_object_id=inbox_object.id
|
||||
)
|
||||
if not outbox_object.id:
|
||||
raise ValueError("Should never happen")
|
||||
|
||||
inbox_object.liked_via_outbox_object_ap_id = outbox_object.ap_id
|
||||
db.commit()
|
||||
await db_session.commit()
|
||||
|
||||
new_outgoing_activity(db, inbox_object.actor.inbox_url, outbox_object.id)
|
||||
await new_outgoing_activity(
|
||||
db_session, inbox_object.actor.inbox_url, outbox_object.id
|
||||
)
|
||||
|
||||
|
||||
def send_announce(db: Session, ap_object_id: str) -> None:
|
||||
inbox_object = get_inbox_object_by_ap_id(db, ap_object_id)
|
||||
async def send_announce(db_session: AsyncSession, ap_object_id: str) -> None:
|
||||
inbox_object = await get_inbox_object_by_ap_id(db_session, ap_object_id)
|
||||
if not inbox_object:
|
||||
raise ValueError(f"{ap_object_id} not found in the inbox")
|
||||
|
||||
|
@ -118,22 +120,22 @@ def send_announce(db: Session, ap_object_id: str) -> None:
|
|||
inbox_object.ap_actor_id,
|
||||
],
|
||||
}
|
||||
outbox_object = save_outbox_object(
|
||||
db, announce_id, announce, relates_to_inbox_object_id=inbox_object.id
|
||||
outbox_object = await save_outbox_object(
|
||||
db_session, announce_id, announce, relates_to_inbox_object_id=inbox_object.id
|
||||
)
|
||||
if not outbox_object.id:
|
||||
raise ValueError("Should never happen")
|
||||
|
||||
inbox_object.announced_via_outbox_object_ap_id = outbox_object.ap_id
|
||||
db.commit()
|
||||
await db_session.commit()
|
||||
|
||||
recipients = _compute_recipients(db, announce)
|
||||
recipients = await _compute_recipients(db_session, announce)
|
||||
for rcp in recipients:
|
||||
new_outgoing_activity(db, rcp, outbox_object.id)
|
||||
await new_outgoing_activity(db_session, rcp, outbox_object.id)
|
||||
|
||||
|
||||
def send_follow(db: Session, ap_actor_id: str) -> None:
|
||||
actor = fetch_actor(db, ap_actor_id)
|
||||
async def send_follow(db_session: AsyncSession, ap_actor_id: str) -> None:
|
||||
actor = await fetch_actor(db_session, ap_actor_id)
|
||||
|
||||
follow_id = allocate_outbox_id()
|
||||
follow = {
|
||||
|
@ -144,17 +146,17 @@ def send_follow(db: Session, ap_actor_id: str) -> None:
|
|||
"object": ap_actor_id,
|
||||
}
|
||||
|
||||
outbox_object = save_outbox_object(
|
||||
db, follow_id, follow, relates_to_actor_id=actor.id
|
||||
outbox_object = await save_outbox_object(
|
||||
db_session, follow_id, follow, relates_to_actor_id=actor.id
|
||||
)
|
||||
if not outbox_object.id:
|
||||
raise ValueError("Should never happen")
|
||||
|
||||
new_outgoing_activity(db, actor.inbox_url, outbox_object.id)
|
||||
await new_outgoing_activity(db_session, actor.inbox_url, outbox_object.id)
|
||||
|
||||
|
||||
def send_undo(db: Session, ap_object_id: str) -> None:
|
||||
outbox_object_to_undo = get_outbox_object_by_ap_id(db, ap_object_id)
|
||||
async def send_undo(db_session: AsyncSession, ap_object_id: str) -> None:
|
||||
outbox_object_to_undo = await get_outbox_object_by_ap_id(db_session, ap_object_id)
|
||||
if not outbox_object_to_undo:
|
||||
raise ValueError(f"{ap_object_id} not found in the outbox")
|
||||
|
||||
|
@ -172,8 +174,8 @@ def send_undo(db: Session, ap_object_id: str) -> None:
|
|||
"object": ap.remove_context(outbox_object_to_undo.ap_object),
|
||||
}
|
||||
|
||||
outbox_object = save_outbox_object(
|
||||
db,
|
||||
outbox_object = await save_outbox_object(
|
||||
db_session,
|
||||
undo_id,
|
||||
undo,
|
||||
relates_to_outbox_object_id=outbox_object_to_undo.id,
|
||||
|
@ -186,31 +188,33 @@ def send_undo(db: Session, ap_object_id: str) -> None:
|
|||
if outbox_object_to_undo.ap_type == "Follow":
|
||||
if not outbox_object_to_undo.activity_object_ap_id:
|
||||
raise ValueError("Should never happen")
|
||||
followed_actor = fetch_actor(db, outbox_object_to_undo.activity_object_ap_id)
|
||||
new_outgoing_activity(
|
||||
db,
|
||||
followed_actor = await fetch_actor(
|
||||
db_session, outbox_object_to_undo.activity_object_ap_id
|
||||
)
|
||||
await new_outgoing_activity(
|
||||
db_session,
|
||||
followed_actor.inbox_url,
|
||||
outbox_object.id,
|
||||
)
|
||||
# Also remove the follow from the following collection
|
||||
db.execute(
|
||||
await db_session.execute(
|
||||
delete(models.Following).where(
|
||||
models.Following.ap_actor_id == followed_actor.ap_id
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
await db_session.commit()
|
||||
elif outbox_object_to_undo.ap_type == "Like":
|
||||
liked_object_ap_id = outbox_object_to_undo.activity_object_ap_id
|
||||
if not liked_object_ap_id:
|
||||
raise ValueError("Should never happen")
|
||||
liked_object = get_inbox_object_by_ap_id(db, liked_object_ap_id)
|
||||
liked_object = await get_inbox_object_by_ap_id(db_session, liked_object_ap_id)
|
||||
if not liked_object:
|
||||
raise ValueError(f"Cannot find liked object {liked_object_ap_id}")
|
||||
liked_object.liked_via_outbox_object_ap_id = None
|
||||
|
||||
# Send the Undo to the liked object's actor
|
||||
new_outgoing_activity(
|
||||
db,
|
||||
await new_outgoing_activity(
|
||||
db_session,
|
||||
liked_object.actor.inbox_url, # type: ignore
|
||||
outbox_object.id,
|
||||
)
|
||||
|
@ -218,21 +222,23 @@ def send_undo(db: Session, ap_object_id: str) -> None:
|
|||
announced_object_ap_id = outbox_object_to_undo.activity_object_ap_id
|
||||
if not announced_object_ap_id:
|
||||
raise ValueError("Should never happen")
|
||||
announced_object = get_inbox_object_by_ap_id(db, announced_object_ap_id)
|
||||
announced_object = await get_inbox_object_by_ap_id(
|
||||
db_session, announced_object_ap_id
|
||||
)
|
||||
if not announced_object:
|
||||
raise ValueError(f"Cannot find announced object {announced_object_ap_id}")
|
||||
announced_object.announced_via_outbox_object_ap_id = None
|
||||
|
||||
# Send the Undo to the original recipients
|
||||
recipients = _compute_recipients(db, outbox_object.ap_object)
|
||||
recipients = await _compute_recipients(db_session, outbox_object.ap_object)
|
||||
for rcp in recipients:
|
||||
new_outgoing_activity(db, rcp, outbox_object.id)
|
||||
await new_outgoing_activity(db_session, rcp, outbox_object.id)
|
||||
else:
|
||||
raise ValueError("Should never happen")
|
||||
|
||||
|
||||
def send_create(
|
||||
db: Session,
|
||||
async def send_create(
|
||||
db_session: AsyncSession,
|
||||
source: str,
|
||||
uploads: list[tuple[models.Upload, str]],
|
||||
in_reply_to: str | None,
|
||||
|
@ -243,11 +249,11 @@ def send_create(
|
|||
note_id = allocate_outbox_id()
|
||||
published = now().replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||||
context = f"{ID}/contexts/" + uuid.uuid4().hex
|
||||
content, tags, mentioned_actors = markdownify(db, source)
|
||||
content, tags, mentioned_actors = await markdownify(db_session, source)
|
||||
attachments = []
|
||||
|
||||
if in_reply_to:
|
||||
in_reply_to_object = get_anybox_object_by_ap_id(db, in_reply_to)
|
||||
in_reply_to_object = await get_anybox_object_by_ap_id(db_session, in_reply_to)
|
||||
if not in_reply_to_object:
|
||||
raise ValueError(f"Invalid in reply to {in_reply_to=}")
|
||||
if not in_reply_to_object.ap_context:
|
||||
|
@ -255,7 +261,7 @@ def send_create(
|
|||
context = in_reply_to_object.ap_context
|
||||
|
||||
if in_reply_to_object.is_from_outbox:
|
||||
db.execute(
|
||||
await db_session.execute(
|
||||
update(models.OutboxObject)
|
||||
.where(
|
||||
models.OutboxObject.ap_id == in_reply_to,
|
||||
|
@ -302,7 +308,7 @@ def send_create(
|
|||
"sensitive": is_sensitive,
|
||||
"attachment": attachments,
|
||||
}
|
||||
outbox_object = save_outbox_object(db, note_id, note, source=source)
|
||||
outbox_object = await save_outbox_object(db_session, note_id, note, source=source)
|
||||
if not outbox_object.id:
|
||||
raise ValueError("Should never happen")
|
||||
|
||||
|
@ -312,24 +318,26 @@ def send_create(
|
|||
tag=tag["name"][1:],
|
||||
outbox_object_id=outbox_object.id,
|
||||
)
|
||||
db.add(tagged_object)
|
||||
db_session.add(tagged_object)
|
||||
|
||||
for (upload, filename) in uploads:
|
||||
outbox_object_attachment = models.OutboxObjectAttachment(
|
||||
filename=filename, outbox_object_id=outbox_object.id, upload_id=upload.id
|
||||
)
|
||||
db.add(outbox_object_attachment)
|
||||
db_session.add(outbox_object_attachment)
|
||||
|
||||
db.commit()
|
||||
await db_session.commit()
|
||||
|
||||
recipients = _compute_recipients(db, note)
|
||||
recipients = await _compute_recipients(db_session, note)
|
||||
for rcp in recipients:
|
||||
new_outgoing_activity(db, rcp, outbox_object.id)
|
||||
await new_outgoing_activity(db_session, rcp, outbox_object.id)
|
||||
|
||||
return note_id
|
||||
|
||||
|
||||
def _compute_recipients(db: Session, ap_object: ap.RawObject) -> set[str]:
|
||||
async def _compute_recipients(
|
||||
db_session: AsyncSession, ap_object: ap.RawObject
|
||||
) -> set[str]:
|
||||
_recipients = []
|
||||
for field in ["to", "cc", "bto", "bcc"]:
|
||||
if field in ap_object:
|
||||
|
@ -343,15 +351,17 @@ def _compute_recipients(db: Session, ap_object: ap.RawObject) -> set[str]:
|
|||
|
||||
# If we got a local collection, assume it's a collection of actors
|
||||
if r.startswith(BASE_URL):
|
||||
for actor in fetch_actor_collection(db, r):
|
||||
for actor in await fetch_actor_collection(db_session, r):
|
||||
recipients.add(actor.shared_inbox_url or actor.inbox_url)
|
||||
|
||||
continue
|
||||
|
||||
# Is it a known actor?
|
||||
known_actor = db.execute(
|
||||
known_actor = (
|
||||
await db_session.execute(
|
||||
select(models.Actor).where(models.Actor.ap_id == r)
|
||||
).scalar_one_or_none()
|
||||
)
|
||||
).scalar_one_or_none() # type: ignore
|
||||
if known_actor:
|
||||
recipients.add(known_actor.shared_inbox_url or known_actor.inbox_url)
|
||||
continue
|
||||
|
@ -359,7 +369,7 @@ def _compute_recipients(db: Session, ap_object: ap.RawObject) -> set[str]:
|
|||
# Fetch the object
|
||||
raw_object = ap.fetch(r)
|
||||
if raw_object.get("type") in ap.ACTOR_TYPES:
|
||||
saved_actor = save_actor(db, raw_object)
|
||||
saved_actor = await save_actor(db_session, raw_object)
|
||||
recipients.add(saved_actor.shared_inbox_url or saved_actor.inbox_url)
|
||||
else:
|
||||
# Assume it's a collection of actors
|
||||
|
@ -370,27 +380,43 @@ def _compute_recipients(db: Session, ap_object: ap.RawObject) -> set[str]:
|
|||
return recipients
|
||||
|
||||
|
||||
def get_inbox_object_by_ap_id(db: Session, ap_id: str) -> models.InboxObject | None:
|
||||
return db.execute(
|
||||
select(models.InboxObject).where(models.InboxObject.ap_id == ap_id)
|
||||
).scalar_one_or_none()
|
||||
async def get_inbox_object_by_ap_id(
|
||||
db_session: AsyncSession, ap_id: str
|
||||
) -> models.InboxObject | None:
|
||||
return (
|
||||
await db_session.execute(
|
||||
select(models.InboxObject)
|
||||
.where(models.InboxObject.ap_id == ap_id)
|
||||
.options(
|
||||
joinedload(models.InboxObject.actor),
|
||||
joinedload(models.InboxObject.relates_to_inbox_object),
|
||||
joinedload(models.InboxObject.relates_to_outbox_object),
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none() # type: ignore
|
||||
|
||||
|
||||
def get_outbox_object_by_ap_id(db: Session, ap_id: str) -> models.OutboxObject | None:
|
||||
return db.execute(
|
||||
async def get_outbox_object_by_ap_id(
|
||||
db_session: AsyncSession, ap_id: str
|
||||
) -> models.OutboxObject | None:
|
||||
return (
|
||||
await db_session.execute(
|
||||
select(models.OutboxObject).where(models.OutboxObject.ap_id == ap_id)
|
||||
).scalar_one_or_none()
|
||||
)
|
||||
).scalar_one_or_none() # type: ignore
|
||||
|
||||
|
||||
def get_anybox_object_by_ap_id(db: Session, ap_id: str) -> AnyboxObject | None:
|
||||
async def get_anybox_object_by_ap_id(
|
||||
db_session: AsyncSession, ap_id: str
|
||||
) -> AnyboxObject | None:
|
||||
if ap_id.startswith(BASE_URL):
|
||||
return get_outbox_object_by_ap_id(db, ap_id)
|
||||
return await get_outbox_object_by_ap_id(db_session, ap_id)
|
||||
else:
|
||||
return get_inbox_object_by_ap_id(db, ap_id)
|
||||
return await get_inbox_object_by_ap_id(db_session, ap_id)
|
||||
|
||||
|
||||
def _handle_delete_activity(
|
||||
db: Session,
|
||||
async def _handle_delete_activity(
|
||||
db_session: AsyncSession,
|
||||
from_actor: models.Actor,
|
||||
ap_object_to_delete: models.InboxObject,
|
||||
) -> None:
|
||||
|
@ -404,12 +430,12 @@ def _handle_delete_activity(
|
|||
# TODO(ts): do we need to delete related activities? should we keep
|
||||
# bookmarked objects with a deleted flag?
|
||||
logger.info(f"Deleting {ap_object_to_delete.ap_type}/{ap_object_to_delete.ap_id}")
|
||||
db.delete(ap_object_to_delete)
|
||||
db.flush()
|
||||
await db_session.delete(ap_object_to_delete)
|
||||
await db_session.flush()
|
||||
|
||||
|
||||
def _handle_follow_follow_activity(
|
||||
db: Session,
|
||||
async def _handle_follow_follow_activity(
|
||||
db_session: AsyncSession,
|
||||
from_actor: models.Actor,
|
||||
inbox_object: models.InboxObject,
|
||||
) -> None:
|
||||
|
@ -419,8 +445,8 @@ def _handle_follow_follow_activity(
|
|||
ap_actor_id=from_actor.ap_id,
|
||||
)
|
||||
try:
|
||||
db.add(follower)
|
||||
db.flush()
|
||||
db_session.add(follower)
|
||||
await db_session.flush()
|
||||
except IntegrityError:
|
||||
pass # TODO update the existing followe
|
||||
|
||||
|
@ -433,20 +459,20 @@ def _handle_follow_follow_activity(
|
|||
"actor": ID,
|
||||
"object": inbox_object.ap_id,
|
||||
}
|
||||
outbox_activity = save_outbox_object(db, reply_id, reply)
|
||||
outbox_activity = await save_outbox_object(db_session, reply_id, reply)
|
||||
if not outbox_activity.id:
|
||||
raise ValueError("Should never happen")
|
||||
new_outgoing_activity(db, from_actor.inbox_url, outbox_activity.id)
|
||||
await new_outgoing_activity(db_session, from_actor.inbox_url, outbox_activity.id)
|
||||
|
||||
notif = models.Notification(
|
||||
notification_type=models.NotificationType.NEW_FOLLOWER,
|
||||
actor_id=from_actor.id,
|
||||
)
|
||||
db.add(notif)
|
||||
db_session.add(notif)
|
||||
|
||||
|
||||
def _handle_undo_activity(
|
||||
db: Session,
|
||||
async def _handle_undo_activity(
|
||||
db_session: AsyncSession,
|
||||
from_actor: models.Actor,
|
||||
undo_activity: models.InboxObject,
|
||||
ap_activity_to_undo: models.InboxObject,
|
||||
|
@ -462,7 +488,7 @@ def _handle_undo_activity(
|
|||
|
||||
if ap_activity_to_undo.ap_type == "Follow":
|
||||
logger.info(f"Undo follow from {from_actor.ap_id}")
|
||||
db.execute(
|
||||
await db_session.execute(
|
||||
delete(models.Follower).where(
|
||||
models.Follower.inbox_object_id == ap_activity_to_undo.id
|
||||
)
|
||||
|
@ -471,13 +497,13 @@ def _handle_undo_activity(
|
|||
notification_type=models.NotificationType.UNFOLLOW,
|
||||
actor_id=from_actor.id,
|
||||
)
|
||||
db.add(notif)
|
||||
db_session.add(notif)
|
||||
|
||||
elif ap_activity_to_undo.ap_type == "Like":
|
||||
if not ap_activity_to_undo.activity_object_ap_id:
|
||||
raise ValueError("Like without object")
|
||||
liked_obj = get_outbox_object_by_ap_id(
|
||||
db,
|
||||
liked_obj = await get_outbox_object_by_ap_id(
|
||||
db_session,
|
||||
ap_activity_to_undo.activity_object_ap_id,
|
||||
)
|
||||
if not liked_obj:
|
||||
|
@ -494,7 +520,7 @@ def _handle_undo_activity(
|
|||
outbox_object_id=liked_obj.id,
|
||||
inbox_object_id=ap_activity_to_undo.id,
|
||||
)
|
||||
db.add(notif)
|
||||
db_session.add(notif)
|
||||
|
||||
elif ap_activity_to_undo.ap_type == "Announce":
|
||||
if not ap_activity_to_undo.activity_object_ap_id:
|
||||
|
@ -504,8 +530,8 @@ def _handle_undo_activity(
|
|||
f"Undo for announce {ap_activity_to_undo.ap_id}/{announced_obj_ap_id}"
|
||||
)
|
||||
if announced_obj_ap_id.startswith(BASE_URL):
|
||||
announced_obj_from_outbox = get_outbox_object_by_ap_id(
|
||||
db, announced_obj_ap_id
|
||||
announced_obj_from_outbox = await get_outbox_object_by_ap_id(
|
||||
db_session, announced_obj_ap_id
|
||||
)
|
||||
if announced_obj_from_outbox:
|
||||
logger.info("Found in the oubox")
|
||||
|
@ -518,7 +544,7 @@ def _handle_undo_activity(
|
|||
outbox_object_id=announced_obj_from_outbox.id,
|
||||
inbox_object_id=ap_activity_to_undo.id,
|
||||
)
|
||||
db.add(notif)
|
||||
db_session.add(notif)
|
||||
|
||||
# FIXME(ts): what to do with ap_activity_to_undo? flag? delete?
|
||||
else:
|
||||
|
@ -527,8 +553,8 @@ def _handle_undo_activity(
|
|||
# commit will be perfomed in save_to_inbox
|
||||
|
||||
|
||||
def _handle_create_activity(
|
||||
db: Session,
|
||||
async def _handle_create_activity(
|
||||
db_session: AsyncSession,
|
||||
from_actor: models.Actor,
|
||||
created_object: models.InboxObject,
|
||||
) -> None:
|
||||
|
@ -544,7 +570,7 @@ def _handle_create_activity(
|
|||
return None
|
||||
|
||||
if created_object.in_reply_to and created_object.in_reply_to.startswith(BASE_URL):
|
||||
db.execute(
|
||||
await db_session.execute(
|
||||
update(models.OutboxObject)
|
||||
.where(
|
||||
models.OutboxObject.ap_id == created_object.in_reply_to,
|
||||
|
@ -559,12 +585,12 @@ def _handle_create_activity(
|
|||
actor_id=from_actor.id,
|
||||
inbox_object_id=created_object.id,
|
||||
)
|
||||
db.add(notif)
|
||||
db_session.add(notif)
|
||||
|
||||
|
||||
def save_to_inbox(db: Session, raw_object: ap.RawObject) -> None:
|
||||
async def save_to_inbox(db_session: AsyncSession, raw_object: ap.RawObject) -> None:
|
||||
try:
|
||||
actor = fetch_actor(db, ap.get_id(raw_object["actor"]))
|
||||
actor = await fetch_actor(db_session, ap.get_id(raw_object["actor"]))
|
||||
except httpx.HTTPStatusError:
|
||||
logger.exception("Failed to fetch actor")
|
||||
return
|
||||
|
@ -576,7 +602,7 @@ def save_to_inbox(db: Session, raw_object: ap.RawObject) -> None:
|
|||
ra = RemoteObject(ap.unwrap_activity(raw_object), actor=actor)
|
||||
|
||||
if (
|
||||
db.scalar(
|
||||
await db_session.scalar(
|
||||
select(func.count(models.InboxObject.id)).where(
|
||||
models.InboxObject.ap_id == ra.ap_id
|
||||
)
|
||||
|
@ -590,13 +616,13 @@ def save_to_inbox(db: Session, raw_object: ap.RawObject) -> None:
|
|||
relates_to_outbox_object: models.OutboxObject | None = None
|
||||
if ra.activity_object_ap_id:
|
||||
if ra.activity_object_ap_id.startswith(BASE_URL):
|
||||
relates_to_outbox_object = get_outbox_object_by_ap_id(
|
||||
db,
|
||||
relates_to_outbox_object = await get_outbox_object_by_ap_id(
|
||||
db_session,
|
||||
ra.activity_object_ap_id,
|
||||
)
|
||||
else:
|
||||
relates_to_inbox_object = get_inbox_object_by_ap_id(
|
||||
db,
|
||||
relates_to_inbox_object = await get_inbox_object_by_ap_id(
|
||||
db_session,
|
||||
ra.activity_object_ap_id,
|
||||
)
|
||||
|
||||
|
@ -625,27 +651,29 @@ def save_to_inbox(db: Session, raw_object: ap.RawObject) -> None:
|
|||
), # TODO: handle mentions
|
||||
)
|
||||
|
||||
db.add(inbox_object)
|
||||
db.flush()
|
||||
db.refresh(inbox_object)
|
||||
db_session.add(inbox_object)
|
||||
await db_session.flush()
|
||||
await db_session.refresh(inbox_object)
|
||||
|
||||
if ra.ap_type == "Note": # TODO: handle create better
|
||||
_handle_create_activity(db, actor, inbox_object)
|
||||
await _handle_create_activity(db_session, actor, inbox_object)
|
||||
elif ra.ap_type == "Update":
|
||||
pass
|
||||
elif ra.ap_type == "Delete":
|
||||
if relates_to_inbox_object:
|
||||
_handle_delete_activity(db, actor, relates_to_inbox_object)
|
||||
await _handle_delete_activity(db_session, actor, relates_to_inbox_object)
|
||||
else:
|
||||
# TODO(ts): handle delete actor
|
||||
logger.info(
|
||||
f"Received a Delete for an unknown object: {ra.activity_object_ap_id}"
|
||||
)
|
||||
elif ra.ap_type == "Follow":
|
||||
_handle_follow_follow_activity(db, actor, inbox_object)
|
||||
await _handle_follow_follow_activity(db_session, actor, inbox_object)
|
||||
elif ra.ap_type == "Undo":
|
||||
if relates_to_inbox_object:
|
||||
_handle_undo_activity(db, actor, inbox_object, relates_to_inbox_object)
|
||||
await _handle_undo_activity(
|
||||
db_session, actor, inbox_object, relates_to_inbox_object
|
||||
)
|
||||
else:
|
||||
logger.info("Received Undo for an unknown activity")
|
||||
elif ra.ap_type in ["Accept", "Reject"]:
|
||||
|
@ -661,7 +689,7 @@ def save_to_inbox(db: Session, raw_object: ap.RawObject) -> None:
|
|||
outbox_object_id=relates_to_outbox_object.id,
|
||||
ap_actor_id=actor.ap_id,
|
||||
)
|
||||
db.add(following)
|
||||
db_session.add(following)
|
||||
else:
|
||||
logger.info(
|
||||
"Received an Accept for an unsupported activity: "
|
||||
|
@ -689,7 +717,7 @@ def save_to_inbox(db: Session, raw_object: ap.RawObject) -> None:
|
|||
outbox_object_id=relates_to_outbox_object.id,
|
||||
inbox_object_id=inbox_object.id,
|
||||
)
|
||||
db.add(notif)
|
||||
db_session.add(notif)
|
||||
elif raw_object["type"] == "Announce":
|
||||
if relates_to_outbox_object:
|
||||
# This is an announce for a local object
|
||||
|
@ -703,7 +731,7 @@ def save_to_inbox(db: Session, raw_object: ap.RawObject) -> None:
|
|||
outbox_object_id=relates_to_outbox_object.id,
|
||||
inbox_object_id=inbox_object.id,
|
||||
)
|
||||
db.add(notif)
|
||||
db_session.add(notif)
|
||||
else:
|
||||
# This is announce for a maybe unknown object
|
||||
if relates_to_inbox_object:
|
||||
|
@ -713,7 +741,9 @@ def save_to_inbox(db: Session, raw_object: ap.RawObject) -> None:
|
|||
if not ra.activity_object_ap_id:
|
||||
raise ValueError("Should never happen")
|
||||
announced_raw_object = ap.fetch(ra.activity_object_ap_id)
|
||||
announced_actor = fetch_actor(db, ap.get_actor_id(announced_raw_object))
|
||||
announced_actor = await fetch_actor(
|
||||
db_session, ap.get_actor_id(announced_raw_object)
|
||||
)
|
||||
announced_object = RemoteObject(announced_raw_object, announced_actor)
|
||||
announced_inbox_object = models.InboxObject(
|
||||
server=urlparse(announced_object.ap_id).netloc,
|
||||
|
@ -727,8 +757,8 @@ def save_to_inbox(db: Session, raw_object: ap.RawObject) -> None:
|
|||
visibility=announced_object.visibility,
|
||||
is_hidden_from_stream=True,
|
||||
)
|
||||
db.add(announced_inbox_object)
|
||||
db.flush()
|
||||
db_session.add(announced_inbox_object)
|
||||
await db_session.flush()
|
||||
inbox_object.relates_to_inbox_object_id = announced_inbox_object.id
|
||||
elif ra.ap_type in ["Like", "Announce"]:
|
||||
if not relates_to_outbox_object:
|
||||
|
@ -749,7 +779,7 @@ def save_to_inbox(db: Session, raw_object: ap.RawObject) -> None:
|
|||
outbox_object_id=relates_to_outbox_object.id,
|
||||
inbox_object_id=inbox_object.id,
|
||||
)
|
||||
db.add(notif)
|
||||
db_session.add(notif)
|
||||
elif raw_object["type"] == "Announce":
|
||||
# TODO(ts): notification
|
||||
relates_to_outbox_object.announces_count = (
|
||||
|
@ -762,18 +792,18 @@ def save_to_inbox(db: Session, raw_object: ap.RawObject) -> None:
|
|||
outbox_object_id=relates_to_outbox_object.id,
|
||||
inbox_object_id=inbox_object.id,
|
||||
)
|
||||
db.add(notif)
|
||||
db_session.add(notif)
|
||||
else:
|
||||
raise ValueError("Should never happen")
|
||||
|
||||
else:
|
||||
logger.warning(f"Received an unknown {inbox_object.ap_type} object")
|
||||
|
||||
db.commit()
|
||||
await db_session.commit()
|
||||
|
||||
|
||||
def public_outbox_objects_count(db: Session) -> int:
|
||||
return db.scalar(
|
||||
async def public_outbox_objects_count(db_session: AsyncSession) -> int:
|
||||
return await db_session.scalar(
|
||||
select(func.count(models.OutboxObject.id)).where(
|
||||
models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC,
|
||||
models.OutboxObject.is_deleted.is_(False),
|
||||
|
@ -781,12 +811,16 @@ def public_outbox_objects_count(db: Session) -> int:
|
|||
)
|
||||
|
||||
|
||||
def fetch_actor_collection(db: Session, url: str) -> list[Actor]:
|
||||
async def fetch_actor_collection(db_session: AsyncSession, url: str) -> list[Actor]:
|
||||
if url.startswith(config.BASE_URL):
|
||||
if url == config.BASE_URL + "/followers":
|
||||
followers = (
|
||||
db.scalars(
|
||||
select(models.Follower).options(joinedload(models.Follower.actor))
|
||||
(
|
||||
await db_session.scalars(
|
||||
select(models.Follower).options(
|
||||
joinedload(models.Follower.actor)
|
||||
)
|
||||
)
|
||||
)
|
||||
.unique()
|
||||
.all()
|
||||
|
@ -806,25 +840,29 @@ class ReplyTreeNode:
|
|||
is_root: bool = False
|
||||
|
||||
|
||||
def get_replies_tree(
|
||||
db: Session,
|
||||
async def get_replies_tree(
|
||||
db_session: AsyncSession,
|
||||
requested_object: AnyboxObject,
|
||||
) -> ReplyTreeNode:
|
||||
# TODO: handle visibility
|
||||
tree_nodes: list[AnyboxObject] = []
|
||||
tree_nodes.extend(
|
||||
db.scalars(
|
||||
(
|
||||
await db_session.scalars(
|
||||
select(models.InboxObject).where(
|
||||
models.InboxObject.ap_context == requested_object.ap_context,
|
||||
)
|
||||
)
|
||||
).all()
|
||||
)
|
||||
tree_nodes.extend(
|
||||
db.scalars(
|
||||
(
|
||||
await db_session.scalars(
|
||||
select(models.OutboxObject).where(
|
||||
models.OutboxObject.ap_context == requested_object.ap_context,
|
||||
models.OutboxObject.is_deleted.is_(False),
|
||||
)
|
||||
)
|
||||
).all()
|
||||
)
|
||||
nodes_by_in_reply_to = defaultdict(list)
|
||||
|
|
|
@ -33,7 +33,7 @@ class Config(pydantic.BaseModel):
|
|||
debug: bool = False
|
||||
|
||||
# Config items to make tests easier
|
||||
sqlalchemy_database_url: str | None = None
|
||||
sqlalchemy_database: str | None = None
|
||||
key_path: str | None = None
|
||||
|
||||
|
||||
|
@ -73,8 +73,8 @@ ID = f"{_SCHEME}://{DOMAIN}"
|
|||
USERNAME = CONFIG.username
|
||||
BASE_URL = ID
|
||||
DEBUG = CONFIG.debug
|
||||
DB_PATH = ROOT_DIR / "data" / "microblogpub.db"
|
||||
SQLALCHEMY_DATABASE_URL = CONFIG.sqlalchemy_database_url or f"sqlite:///{DB_PATH}"
|
||||
DB_PATH = CONFIG.sqlalchemy_database or ROOT_DIR / "data" / "microblogpub.db"
|
||||
SQLALCHEMY_DATABASE_URL = f"sqlite:///{DB_PATH}"
|
||||
KEY_PATH = (
|
||||
(ROOT_DIR / CONFIG.key_path) if CONFIG.key_path else ROOT_DIR / "data" / "key.pem"
|
||||
)
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import datetime
|
||||
from typing import Any
|
||||
from typing import Generator
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from app.config import DB_PATH
|
||||
from app.config import SQLALCHEMY_DATABASE_URL
|
||||
|
||||
engine = create_engine(
|
||||
|
@ -14,6 +16,10 @@ engine = create_engine(
|
|||
)
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
DATABASE_URL = f"sqlite+aiosqlite:///{DB_PATH}"
|
||||
async_engine = create_async_engine(DATABASE_URL, future=True, echo=False)
|
||||
async_session = sessionmaker(async_engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
Base: Any = declarative_base()
|
||||
|
||||
|
||||
|
@ -21,9 +27,6 @@ def now() -> datetime.datetime:
|
|||
return datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
|
||||
def get_db() -> Generator[Session, None, None]:
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
async def get_db_session() -> AsyncGenerator[AsyncSession, None]:
|
||||
async with async_session() as session:
|
||||
yield session
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import mf2py # type: ignore
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import activitypub as ap
|
||||
from app import webfinger
|
||||
from app.actor import Actor
|
||||
from app.actor import fetch_actor
|
||||
from app.ap_object import RemoteObject
|
||||
from app.database import AsyncSession
|
||||
|
||||
|
||||
def lookup(db: Session, query: str) -> Actor | RemoteObject:
|
||||
async def lookup(db_session: AsyncSession, query: str) -> Actor | RemoteObject:
|
||||
if query.startswith("@"):
|
||||
query = webfinger.get_actor_url(query) # type: ignore # None check below
|
||||
|
||||
|
@ -34,7 +34,7 @@ def lookup(db: Session, query: str) -> Actor | RemoteObject:
|
|||
raise
|
||||
|
||||
if ap_obj["type"] in ap.ACTOR_TYPES:
|
||||
actor = fetch_actor(db, ap_obj["id"])
|
||||
actor = await fetch_actor(db_session, ap_obj["id"])
|
||||
return actor
|
||||
else:
|
||||
return RemoteObject(ap_obj)
|
||||
|
|
218
app/main.py
218
app/main.py
|
@ -24,7 +24,6 @@ from loguru import logger
|
|||
from PIL import Image
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm import joinedload
|
||||
from starlette.background import BackgroundTask
|
||||
from starlette.responses import JSONResponse
|
||||
|
@ -49,7 +48,8 @@ from app.config import USERNAME
|
|||
from app.config import generate_csrf_token
|
||||
from app.config import is_activitypub_requested
|
||||
from app.config import verify_csrf_token
|
||||
from app.database import get_db
|
||||
from app.database import AsyncSession
|
||||
from app.database import get_db_session
|
||||
from app.templates import is_current_user_admin
|
||||
from app.uploads import UPLOAD_DIR
|
||||
from app.utils import pagination
|
||||
|
@ -139,9 +139,9 @@ class ActivityPubResponse(JSONResponse):
|
|||
|
||||
|
||||
@app.get("/")
|
||||
def index(
|
||||
async def index(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
|
||||
page: int | None = None,
|
||||
) -> templates.TemplateResponse | ActivityPubResponse:
|
||||
|
@ -155,12 +155,13 @@ def index(
|
|||
models.OutboxObject.is_hidden_from_homepage.is_(False),
|
||||
)
|
||||
q = select(models.OutboxObject).where(*where)
|
||||
total_count = db.scalar(select(func.count(models.OutboxObject.id)).where(*where))
|
||||
total_count = await db_session.scalar(
|
||||
select(func.count(models.OutboxObject.id)).where(*where)
|
||||
)
|
||||
page_size = 20
|
||||
page_offset = (page - 1) * page_size
|
||||
|
||||
outbox_objects = (
|
||||
db.scalars(
|
||||
outbox_objects_result = await db_session.scalars(
|
||||
q.options(
|
||||
joinedload(models.OutboxObject.outbox_object_attachments).options(
|
||||
joinedload(models.OutboxObjectAttachment.upload)
|
||||
|
@ -170,12 +171,10 @@ def index(
|
|||
.offset(page_offset)
|
||||
.limit(page_size)
|
||||
)
|
||||
.unique()
|
||||
.all()
|
||||
)
|
||||
outbox_objects = outbox_objects_result.unique().all()
|
||||
|
||||
return templates.render_template(
|
||||
db,
|
||||
return await templates.render_template(
|
||||
db_session,
|
||||
request,
|
||||
"index.html",
|
||||
{
|
||||
|
@ -188,14 +187,14 @@ def index(
|
|||
)
|
||||
|
||||
|
||||
def _build_followx_collection(
|
||||
db: Session,
|
||||
async def _build_followx_collection(
|
||||
db_session: AsyncSession,
|
||||
model_cls: Type[models.Following | models.Follower],
|
||||
path: str,
|
||||
page: bool | None,
|
||||
next_cursor: str | None,
|
||||
) -> ap.RawObject:
|
||||
total_items = db.query(model_cls).count()
|
||||
total_items = await db_session.scalar(select(func.count(model_cls.id)))
|
||||
|
||||
if not page and not next_cursor:
|
||||
return {
|
||||
|
@ -213,11 +212,11 @@ def _build_followx_collection(
|
|||
)
|
||||
q = q.limit(20)
|
||||
|
||||
items = [followx for followx in db.scalars(q).all()]
|
||||
items = [followx for followx in (await db_session.scalars(q)).all()]
|
||||
next_cursor = None
|
||||
if (
|
||||
items
|
||||
and db.scalar(
|
||||
and await db_session.scalar(
|
||||
select(func.count(model_cls.id)).where(
|
||||
model_cls.created_at < items[-1].created_at
|
||||
)
|
||||
|
@ -244,18 +243,18 @@ def _build_followx_collection(
|
|||
|
||||
|
||||
@app.get("/followers")
|
||||
def followers(
|
||||
async def followers(
|
||||
request: Request,
|
||||
page: bool | None = None,
|
||||
next_cursor: str | None = None,
|
||||
prev_cursor: str | None = None,
|
||||
db: Session = Depends(get_db),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
|
||||
) -> ActivityPubResponse | templates.TemplateResponse:
|
||||
if is_activitypub_requested(request):
|
||||
return ActivityPubResponse(
|
||||
_build_followx_collection(
|
||||
db=db,
|
||||
await _build_followx_collection(
|
||||
db_session=db_session,
|
||||
model_cls=models.Follower,
|
||||
path="/followers",
|
||||
page=page,
|
||||
|
@ -264,26 +263,23 @@ def followers(
|
|||
)
|
||||
|
||||
# We only show the most recent 20 followers on the public website
|
||||
followers = (
|
||||
db.scalars(
|
||||
followers_result = await db_session.scalars(
|
||||
select(models.Follower)
|
||||
.options(joinedload(models.Follower.actor))
|
||||
.order_by(models.Follower.created_at.desc())
|
||||
.limit(20)
|
||||
)
|
||||
.unique()
|
||||
.all()
|
||||
)
|
||||
followers = followers_result.unique().all()
|
||||
|
||||
actors_metadata = {}
|
||||
if is_current_user_admin(request):
|
||||
actors_metadata = get_actors_metadata(
|
||||
db,
|
||||
actors_metadata = await get_actors_metadata(
|
||||
db_session,
|
||||
[f.actor for f in followers],
|
||||
)
|
||||
|
||||
return templates.render_template(
|
||||
db,
|
||||
return await templates.render_template(
|
||||
db_session,
|
||||
request,
|
||||
"followers.html",
|
||||
{
|
||||
|
@ -294,18 +290,18 @@ def followers(
|
|||
|
||||
|
||||
@app.get("/following")
|
||||
def following(
|
||||
async def following(
|
||||
request: Request,
|
||||
page: bool | None = None,
|
||||
next_cursor: str | None = None,
|
||||
prev_cursor: str | None = None,
|
||||
db: Session = Depends(get_db),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
|
||||
) -> ActivityPubResponse | templates.TemplateResponse:
|
||||
if is_activitypub_requested(request):
|
||||
return ActivityPubResponse(
|
||||
_build_followx_collection(
|
||||
db=db,
|
||||
await _build_followx_collection(
|
||||
db_session=db_session,
|
||||
model_cls=models.Following,
|
||||
path="/following",
|
||||
page=page,
|
||||
|
@ -315,11 +311,13 @@ def following(
|
|||
|
||||
# We only show the most recent 20 follows on the public website
|
||||
following = (
|
||||
db.scalars(
|
||||
(
|
||||
await db_session.scalars(
|
||||
select(models.Following)
|
||||
.options(joinedload(models.Following.actor))
|
||||
.order_by(models.Following.created_at.desc())
|
||||
)
|
||||
)
|
||||
.unique()
|
||||
.all()
|
||||
)
|
||||
|
@ -327,13 +325,13 @@ def following(
|
|||
# TODO: support next_cursor/prev_cursor
|
||||
actors_metadata = {}
|
||||
if is_current_user_admin(request):
|
||||
actors_metadata = get_actors_metadata(
|
||||
db,
|
||||
actors_metadata = await get_actors_metadata(
|
||||
db_session,
|
||||
[f.actor for f in following],
|
||||
)
|
||||
|
||||
return templates.render_template(
|
||||
db,
|
||||
return await templates.render_template(
|
||||
db_session,
|
||||
request,
|
||||
"following.html",
|
||||
{
|
||||
|
@ -344,12 +342,13 @@ def following(
|
|||
|
||||
|
||||
@app.get("/outbox")
|
||||
def outbox(
|
||||
db: Session = Depends(get_db),
|
||||
async def outbox(
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
|
||||
) -> ActivityPubResponse:
|
||||
# By design, we only show the last 20 public activities in the oubox
|
||||
outbox_objects = db.scalars(
|
||||
outbox_objects = (
|
||||
await db_session.scalars(
|
||||
select(models.OutboxObject)
|
||||
.where(
|
||||
models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC,
|
||||
|
@ -357,6 +356,7 @@ def outbox(
|
|||
)
|
||||
.order_by(models.OutboxObject.ap_published_at.desc())
|
||||
.limit(20)
|
||||
)
|
||||
).all()
|
||||
return ActivityPubResponse(
|
||||
{
|
||||
|
@ -373,11 +373,12 @@ def outbox(
|
|||
|
||||
|
||||
@app.get("/featured")
|
||||
def featured(
|
||||
db: Session = Depends(get_db),
|
||||
async def featured(
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
|
||||
) -> ActivityPubResponse:
|
||||
outbox_objects = db.scalars(
|
||||
outbox_objects = (
|
||||
await db_session.scalars(
|
||||
select(models.OutboxObject)
|
||||
.filter(
|
||||
models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC,
|
||||
|
@ -386,6 +387,7 @@ def featured(
|
|||
)
|
||||
.order_by(models.OutboxObject.ap_published_at.desc())
|
||||
.limit(5)
|
||||
)
|
||||
).all()
|
||||
return ActivityPubResponse(
|
||||
{
|
||||
|
@ -398,9 +400,9 @@ def featured(
|
|||
)
|
||||
|
||||
|
||||
def _check_outbox_object_acl(
|
||||
async def _check_outbox_object_acl(
|
||||
request: Request,
|
||||
db: Session,
|
||||
db_session: AsyncSession,
|
||||
ap_object: models.OutboxObject,
|
||||
httpsig_info: httpsig.HTTPSigInfo,
|
||||
) -> None:
|
||||
|
@ -413,7 +415,9 @@ def _check_outbox_object_acl(
|
|||
]:
|
||||
return None
|
||||
elif ap_object.visibility == ap.VisibilityEnum.FOLLOWERS_ONLY:
|
||||
followers = boxes.fetch_actor_collection(db, BASE_URL + "/followers")
|
||||
followers = await boxes.fetch_actor_collection(
|
||||
db_session, BASE_URL + "/followers"
|
||||
)
|
||||
if httpsig_info.signed_by_ap_actor_id in [actor.ap_id for actor in followers]:
|
||||
return None
|
||||
elif ap_object.visibility == ap.VisibilityEnum.DIRECT:
|
||||
|
@ -425,14 +429,15 @@ def _check_outbox_object_acl(
|
|||
|
||||
|
||||
@app.get("/o/{public_id}")
|
||||
def outbox_by_public_id(
|
||||
async def outbox_by_public_id(
|
||||
public_id: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
httpsig_info: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
|
||||
) -> ActivityPubResponse | templates.TemplateResponse:
|
||||
maybe_object = (
|
||||
db.execute(
|
||||
(
|
||||
await db_session.execute(
|
||||
select(models.OutboxObject)
|
||||
.options(
|
||||
joinedload(models.OutboxObject.outbox_object_attachments).options(
|
||||
|
@ -444,21 +449,23 @@ def outbox_by_public_id(
|
|||
models.OutboxObject.is_deleted.is_(False),
|
||||
)
|
||||
)
|
||||
)
|
||||
.unique()
|
||||
.scalar_one_or_none()
|
||||
)
|
||||
if not maybe_object:
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
_check_outbox_object_acl(request, db, maybe_object, httpsig_info)
|
||||
await _check_outbox_object_acl(request, db_session, maybe_object, httpsig_info)
|
||||
|
||||
if is_activitypub_requested(request):
|
||||
return ActivityPubResponse(maybe_object.ap_object)
|
||||
|
||||
replies_tree = boxes.get_replies_tree(db, maybe_object)
|
||||
replies_tree = await boxes.get_replies_tree(db_session, maybe_object)
|
||||
|
||||
likes = (
|
||||
db.scalars(
|
||||
(
|
||||
await db_session.scalars(
|
||||
select(models.InboxObject)
|
||||
.where(
|
||||
models.InboxObject.ap_type == "Like",
|
||||
|
@ -468,12 +475,14 @@ def outbox_by_public_id(
|
|||
.order_by(models.InboxObject.ap_published_at.desc())
|
||||
.limit(10)
|
||||
)
|
||||
)
|
||||
.unique()
|
||||
.all()
|
||||
)
|
||||
|
||||
shares = (
|
||||
db.scalars(
|
||||
(
|
||||
await db_session.scalars(
|
||||
select(models.InboxObject)
|
||||
.filter(
|
||||
models.InboxObject.ap_type == "Announce",
|
||||
|
@ -483,12 +492,13 @@ def outbox_by_public_id(
|
|||
.order_by(models.InboxObject.ap_published_at.desc())
|
||||
.limit(10)
|
||||
)
|
||||
)
|
||||
.unique()
|
||||
.all()
|
||||
)
|
||||
|
||||
return templates.render_template(
|
||||
db,
|
||||
return await templates.render_template(
|
||||
db_session,
|
||||
request,
|
||||
"object.html",
|
||||
{
|
||||
|
@ -501,31 +511,33 @@ def outbox_by_public_id(
|
|||
|
||||
|
||||
@app.get("/o/{public_id}/activity")
|
||||
def outbox_activity_by_public_id(
|
||||
async def outbox_activity_by_public_id(
|
||||
public_id: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
httpsig_info: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
|
||||
) -> ActivityPubResponse:
|
||||
maybe_object = db.execute(
|
||||
maybe_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 maybe_object:
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
_check_outbox_object_acl(request, db, maybe_object, httpsig_info)
|
||||
await _check_outbox_object_acl(request, db_session, maybe_object, httpsig_info)
|
||||
|
||||
return ActivityPubResponse(ap.wrap_object(maybe_object.ap_object))
|
||||
|
||||
|
||||
@app.get("/t/{tag}")
|
||||
def tag_by_name(
|
||||
async def tag_by_name(
|
||||
tag: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
|
||||
) -> ActivityPubResponse | templates.TemplateResponse:
|
||||
# TODO(ts): implement HTML version
|
||||
|
@ -554,23 +566,23 @@ def emoji_by_name(name: str) -> ActivityPubResponse:
|
|||
@app.post("/inbox")
|
||||
async def inbox(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
httpsig_info: httpsig.HTTPSigInfo = Depends(httpsig.enforce_httpsig),
|
||||
) -> Response:
|
||||
logger.info(f"headers={request.headers}")
|
||||
payload = await request.json()
|
||||
logger.info(f"{payload=}")
|
||||
save_to_inbox(db, payload)
|
||||
await save_to_inbox(db_session, payload)
|
||||
return Response(status_code=204)
|
||||
|
||||
|
||||
@app.get("/remote_follow")
|
||||
def get_remote_follow(
|
||||
async def get_remote_follow(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> templates.TemplateResponse:
|
||||
return templates.render_template(
|
||||
db,
|
||||
return await templates.render_template(
|
||||
db_session,
|
||||
request,
|
||||
"remote_follow.html",
|
||||
{"remote_follow_csrf_token": generate_csrf_token()},
|
||||
|
@ -578,9 +590,8 @@ def get_remote_follow(
|
|||
|
||||
|
||||
@app.post("/remote_follow")
|
||||
def post_remote_follow(
|
||||
async def post_remote_follow(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
csrf_check: None = Depends(verify_csrf_token),
|
||||
profile: str = Form(),
|
||||
) -> RedirectResponse:
|
||||
|
@ -598,7 +609,7 @@ def post_remote_follow(
|
|||
|
||||
|
||||
@app.get("/.well-known/webfinger")
|
||||
def wellknown_webfinger(resource: str) -> JSONResponse:
|
||||
async def wellknown_webfinger(resource: str) -> JSONResponse:
|
||||
"""Exposes/servers WebFinger data."""
|
||||
omg = f"acct:{USERNAME}@{DOMAIN}"
|
||||
logger.info(f"{resource == omg}/{resource}/{omg}/{len(resource)}/{len(omg)}")
|
||||
|
@ -639,10 +650,10 @@ async def well_known_nodeinfo() -> dict[str, Any]:
|
|||
|
||||
|
||||
@app.get("/nodeinfo")
|
||||
def nodeinfo(
|
||||
db: Session = Depends(get_db),
|
||||
async def nodeinfo(
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
local_posts = public_outbox_objects_count(db)
|
||||
local_posts = await public_outbox_objects_count(db_session)
|
||||
return JSONResponse(
|
||||
{
|
||||
"version": "2.1",
|
||||
|
@ -780,15 +791,17 @@ def serve_proxy_media_resized(
|
|||
|
||||
|
||||
@app.get("/attachments/{content_hash}/{filename}")
|
||||
def serve_attachment(
|
||||
async def serve_attachment(
|
||||
content_hash: str,
|
||||
filename: str,
|
||||
db: Session = Depends(get_db),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
upload = db.execute(
|
||||
upload = (
|
||||
await db_session.execute(
|
||||
select(models.Upload).where(
|
||||
models.Upload.content_hash == content_hash,
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if not upload:
|
||||
raise HTTPException(status_code=404)
|
||||
|
@ -800,15 +813,17 @@ def serve_attachment(
|
|||
|
||||
|
||||
@app.get("/attachments/thumbnails/{content_hash}/{filename}")
|
||||
def serve_attachment_thumbnail(
|
||||
async def serve_attachment_thumbnail(
|
||||
content_hash: str,
|
||||
filename: str,
|
||||
db: Session = Depends(get_db),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
upload = db.execute(
|
||||
upload = (
|
||||
await db_session.execute(
|
||||
select(models.Upload).where(
|
||||
models.Upload.content_hash == content_hash,
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if not upload or not upload.has_thumbnail:
|
||||
raise HTTPException(status_code=404)
|
||||
|
@ -827,24 +842,35 @@ Disallow: /following
|
|||
Disallow: /admin"""
|
||||
|
||||
|
||||
def _get_outbox_for_feed(db: Session) -> list[models.OutboxObject]:
|
||||
return db.scalars(
|
||||
async def _get_outbox_for_feed(db_session: AsyncSession) -> list[models.OutboxObject]:
|
||||
return (
|
||||
(
|
||||
await db_session.scalars(
|
||||
select(models.OutboxObject)
|
||||
.where(
|
||||
models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC,
|
||||
models.OutboxObject.is_deleted.is_(False),
|
||||
models.OutboxObject.ap_type.in_(["Note", "Article", "Video"]),
|
||||
)
|
||||
.options(
|
||||
joinedload(models.OutboxObject.outbox_object_attachments).options(
|
||||
joinedload(models.OutboxObjectAttachment.upload)
|
||||
)
|
||||
)
|
||||
.order_by(models.OutboxObject.ap_published_at.desc())
|
||||
.limit(20)
|
||||
).all()
|
||||
)
|
||||
)
|
||||
.unique()
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
@app.get("/feed.json")
|
||||
def json_feed(
|
||||
db: Session = Depends(get_db),
|
||||
async def json_feed(
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict[str, Any]:
|
||||
outbox_objects = _get_outbox_for_feed(db)
|
||||
outbox_objects = await _get_outbox_for_feed(db_session)
|
||||
data = []
|
||||
for outbox_object in outbox_objects:
|
||||
if not outbox_object.ap_published_at:
|
||||
|
@ -876,8 +902,8 @@ def json_feed(
|
|||
}
|
||||
|
||||
|
||||
def _gen_rss_feed(
|
||||
db: Session,
|
||||
async def _gen_rss_feed(
|
||||
db_session: AsyncSession,
|
||||
):
|
||||
fg = FeedGenerator()
|
||||
fg.id(BASE_URL + "/feed.rss")
|
||||
|
@ -888,7 +914,7 @@ def _gen_rss_feed(
|
|||
fg.logo(LOCAL_ACTOR.icon_url)
|
||||
fg.language("en")
|
||||
|
||||
outbox_objects = _get_outbox_for_feed(db)
|
||||
outbox_objects = await _get_outbox_for_feed(db_session)
|
||||
for outbox_object in outbox_objects:
|
||||
if not outbox_object.ap_published_at:
|
||||
raise ValueError(f"{outbox_object} has no published date")
|
||||
|
@ -904,20 +930,20 @@ def _gen_rss_feed(
|
|||
|
||||
|
||||
@app.get("/feed.rss")
|
||||
def rss_feed(
|
||||
db: Session = Depends(get_db),
|
||||
async def rss_feed(
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> PlainTextResponse:
|
||||
return PlainTextResponse(
|
||||
_gen_rss_feed(db).rss_str(),
|
||||
(await _gen_rss_feed(db_session)).rss_str(),
|
||||
headers={"Content-Type": "application/rss+xml"},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/feed.atom")
|
||||
def atom_feed(
|
||||
db: Session = Depends(get_db),
|
||||
async def atom_feed(
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> PlainTextResponse:
|
||||
return PlainTextResponse(
|
||||
_gen_rss_feed(db).atom_str(),
|
||||
(await _gen_rss_feed(db_session)).atom_str(),
|
||||
headers={"Content-Type": "application/atom+xml"},
|
||||
)
|
||||
|
|
|
@ -11,15 +11,23 @@ from sqlalchemy import select
|
|||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import activitypub as ap
|
||||
from app import config
|
||||
from app import ldsig
|
||||
from app import models
|
||||
from app.database import AsyncSession
|
||||
from app.database import SessionLocal
|
||||
from app.database import now
|
||||
from app.key import Key
|
||||
from app.key import get_key
|
||||
|
||||
_MAX_RETRIES = 16
|
||||
|
||||
k = Key(config.ID, f"{config.ID}#main-key")
|
||||
k.load(get_key())
|
||||
|
||||
def new_outgoing_activity(
|
||||
db: Session,
|
||||
|
||||
async def new_outgoing_activity(
|
||||
db_session: AsyncSession,
|
||||
recipient: str,
|
||||
outbox_object_id: int,
|
||||
) -> models.OutgoingActivity:
|
||||
|
@ -28,9 +36,9 @@ def new_outgoing_activity(
|
|||
outbox_object_id=outbox_object_id,
|
||||
)
|
||||
|
||||
db.add(outgoing_activity)
|
||||
db.commit()
|
||||
db.refresh(outgoing_activity)
|
||||
db_session.add(outgoing_activity)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(outgoing_activity)
|
||||
return outgoing_activity
|
||||
|
||||
|
||||
|
@ -91,6 +99,8 @@ def process_next_outgoing_activity(db: Session) -> bool:
|
|||
next_activity.last_try = now()
|
||||
|
||||
payload = ap.wrap_object_if_needed(next_activity.outbox_object.ap_object)
|
||||
if payload["type"] == "Create":
|
||||
ldsig.generate_signature(payload, k)
|
||||
logger.info(f"{payload=}")
|
||||
try:
|
||||
resp = ap.post(next_activity.recipient, payload)
|
||||
|
|
|
@ -2,13 +2,13 @@ import re
|
|||
|
||||
from markdown import markdown
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import models
|
||||
from app import webfinger
|
||||
from app.actor import Actor
|
||||
from app.actor import fetch_actor
|
||||
from app.config import BASE_URL
|
||||
from app.database import AsyncSession
|
||||
from app.utils import emoji
|
||||
|
||||
|
||||
|
@ -24,7 +24,9 @@ _HASHTAG_REGEX = re.compile(r"(#[\d\w]+)")
|
|||
_MENTION_REGEX = re.compile(r"@[\d\w_.+-]+@[\d\w-]+\.[\d\w\-.]+")
|
||||
|
||||
|
||||
def _hashtagify(db: Session, content: str) -> tuple[str, list[dict[str, str]]]:
|
||||
async def _hashtagify(
|
||||
db_session: AsyncSession, content: str
|
||||
) -> tuple[str, list[dict[str, str]]]:
|
||||
tags = []
|
||||
hashtags = re.findall(_HASHTAG_REGEX, content)
|
||||
hashtags = sorted(set(hashtags), reverse=True) # unique tags, longest first
|
||||
|
@ -36,23 +38,25 @@ def _hashtagify(db: Session, content: str) -> tuple[str, list[dict[str, str]]]:
|
|||
return content, tags
|
||||
|
||||
|
||||
def _mentionify(
|
||||
db: Session,
|
||||
async def _mentionify(
|
||||
db_session: AsyncSession,
|
||||
content: str,
|
||||
) -> tuple[str, list[dict[str, str]], list[Actor]]:
|
||||
tags = []
|
||||
mentioned_actors = []
|
||||
for mention in re.findall(_MENTION_REGEX, content):
|
||||
_, username, domain = mention.split("@")
|
||||
actor = db.execute(
|
||||
actor = (
|
||||
await db_session.execute(
|
||||
select(models.Actor).where(models.Actor.handle == mention)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if not actor:
|
||||
actor_url = webfinger.get_actor_url(mention)
|
||||
if not actor_url:
|
||||
# FIXME(ts): raise an error?
|
||||
continue
|
||||
actor = fetch_actor(db, actor_url)
|
||||
actor = await fetch_actor(db_session, actor_url)
|
||||
|
||||
mentioned_actors.append(actor)
|
||||
tags.append(dict(type="Mention", href=actor.url, name=mention))
|
||||
|
@ -62,8 +66,8 @@ def _mentionify(
|
|||
return content, tags, mentioned_actors
|
||||
|
||||
|
||||
def markdownify(
|
||||
db: Session,
|
||||
async def markdownify(
|
||||
db_session: AsyncSession,
|
||||
content: str,
|
||||
mentionify: bool = True,
|
||||
hashtagify: bool = True,
|
||||
|
@ -75,10 +79,10 @@ def markdownify(
|
|||
tags = []
|
||||
mentioned_actors: list[Actor] = []
|
||||
if hashtagify:
|
||||
content, hashtag_tags = _hashtagify(db, content)
|
||||
content, hashtag_tags = await _hashtagify(db_session, content)
|
||||
tags.extend(hashtag_tags)
|
||||
if mentionify:
|
||||
content, mention_tags, mentioned_actors = _mentionify(db, content)
|
||||
content, mention_tags, mentioned_actors = await _mentionify(db_session, content)
|
||||
tags.extend(mention_tags)
|
||||
|
||||
# Handle custom emoji
|
||||
|
|
|
@ -15,7 +15,6 @@ from fastapi.templating import Jinja2Templates
|
|||
from loguru import logger
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from starlette.templating import _TemplateResponse as TemplateResponse
|
||||
|
||||
from app import activitypub as ap
|
||||
|
@ -29,6 +28,7 @@ from app.config import DEBUG
|
|||
from app.config import VERSION
|
||||
from app.config import generate_csrf_token
|
||||
from app.config import session_serializer
|
||||
from app.database import AsyncSession
|
||||
from app.database import now
|
||||
from app.media import proxied_media_url
|
||||
from app.utils.highlight import HIGHLIGHT_CSS
|
||||
|
@ -77,8 +77,8 @@ def is_current_user_admin(request: Request) -> bool:
|
|||
return is_admin
|
||||
|
||||
|
||||
def render_template(
|
||||
db: Session,
|
||||
async def render_template(
|
||||
db_session: AsyncSession,
|
||||
request: Request,
|
||||
template: str,
|
||||
template_args: dict[str, Any] = {},
|
||||
|
@ -96,7 +96,7 @@ def render_template(
|
|||
"csrf_token": generate_csrf_token() if is_admin else None,
|
||||
"highlight_css": HIGHLIGHT_CSS,
|
||||
"visibility_enum": ap.VisibilityEnum,
|
||||
"notifications_count": db.scalar(
|
||||
"notifications_count": await db_session.scalar(
|
||||
select(func.count(models.Notification.id)).where(
|
||||
models.Notification.is_new.is_(True)
|
||||
)
|
||||
|
@ -104,8 +104,12 @@ def render_template(
|
|||
if is_admin
|
||||
else 0,
|
||||
"local_actor": LOCAL_ACTOR,
|
||||
"followers_count": db.scalar(select(func.count(models.Follower.id))),
|
||||
"following_count": db.scalar(select(func.count(models.Following.id))),
|
||||
"followers_count": await db_session.scalar(
|
||||
select(func.count(models.Follower.id))
|
||||
),
|
||||
"following_count": await db_session.scalar(
|
||||
select(func.count(models.Following.id))
|
||||
),
|
||||
**template_args,
|
||||
},
|
||||
)
|
||||
|
|
|
@ -11,12 +11,12 @@ from app import activitypub as ap
|
|||
from app import models
|
||||
from app.config import BASE_URL
|
||||
from app.config import ROOT_DIR
|
||||
from app.database import Session
|
||||
from app.database import AsyncSession
|
||||
|
||||
UPLOAD_DIR = ROOT_DIR / "data" / "uploads"
|
||||
|
||||
|
||||
def save_upload(db: Session, f: UploadFile) -> models.Upload:
|
||||
async def save_upload(db_session: AsyncSession, f: UploadFile) -> models.Upload:
|
||||
# Compute the hash
|
||||
h = hashlib.blake2b(digest_size=32)
|
||||
while True:
|
||||
|
@ -28,8 +28,10 @@ def save_upload(db: Session, f: UploadFile) -> models.Upload:
|
|||
content_hash = h.hexdigest()
|
||||
f.file.seek(0)
|
||||
|
||||
existing_upload = db.execute(
|
||||
existing_upload = (
|
||||
await db_session.execute(
|
||||
select(models.Upload).where(models.Upload.content_hash == content_hash)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if existing_upload:
|
||||
logger.info(f"Upload with {content_hash=} already exists")
|
||||
|
@ -88,8 +90,8 @@ def save_upload(db: Session, f: UploadFile) -> models.Upload:
|
|||
width=width,
|
||||
height=height,
|
||||
)
|
||||
db.add(new_upload)
|
||||
db.commit()
|
||||
db_session.add(new_upload)
|
||||
await db_session.commit()
|
||||
|
||||
return new_upload
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ secret = "1dd4079e0474d1a519052b8fe3cb5fa6"
|
|||
debug = true
|
||||
|
||||
# In-mem DB
|
||||
sqlalchemy_database_url = "sqlite:///file:pytest?mode=memory&cache=shared&uri=true"
|
||||
# sqlalchemy_database_url = "sqlite:///data/pytest.db"
|
||||
sqlalchemy_database = "file:pytest?mode=memory&cache=shared&uri=true"
|
||||
# sqlalchemy_database_url = "data/pytest.db"
|
||||
key_path = "tests/test.key"
|
||||
media_db_path = "tests/media.db"
|
||||
|
|
103
poetry.lock
generated
103
poetry.lock
generated
|
@ -1,3 +1,14 @@
|
|||
[[package]]
|
||||
name = "aiosqlite"
|
||||
version = "0.17.0"
|
||||
description = "asyncio bridge to the standard sqlite3 module"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
typing_extensions = ">=3.7.2"
|
||||
|
||||
[[package]]
|
||||
name = "alembic"
|
||||
version = "1.8.0"
|
||||
|
@ -590,7 +601,7 @@ requests = ">=2.18.4"
|
|||
name = "mypy"
|
||||
version = "0.960"
|
||||
description = "Optional static typing for Python"
|
||||
category = "main"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
|
@ -608,7 +619,7 @@ reports = ["lxml"]
|
|||
name = "mypy-extensions"
|
||||
version = "0.4.3"
|
||||
description = "Experimental type system extensions for programs checked with the mypy typechecker."
|
||||
category = "main"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
|
@ -916,7 +927,7 @@ python-versions = ">=3.6"
|
|||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "1.4.37"
|
||||
version = "1.4.39"
|
||||
description = "Database Abstraction Library"
|
||||
category = "main"
|
||||
optional = false
|
||||
|
@ -924,8 +935,6 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
|
|||
|
||||
[package.dependencies]
|
||||
greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"}
|
||||
mypy = {version = ">=0.910", optional = true, markers = "python_version >= \"3\" and extra == \"mypy\""}
|
||||
sqlalchemy2-stubs = {version = "*", optional = true, markers = "extra == \"mypy\""}
|
||||
|
||||
[package.extras]
|
||||
aiomysql = ["greenlet (!=0.4.17)", "aiomysql"]
|
||||
|
@ -950,7 +959,7 @@ sqlcipher = ["sqlcipher3-binary"]
|
|||
|
||||
[[package]]
|
||||
name = "sqlalchemy2-stubs"
|
||||
version = "0.0.2a23"
|
||||
version = "0.0.2a24"
|
||||
description = "Typing Stubs for SQLAlchemy 1.4"
|
||||
category = "main"
|
||||
optional = false
|
||||
|
@ -1134,9 +1143,13 @@ dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"]
|
|||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "e8f20d21a8c7822fbc3c183376d694fc0109e90851377bc6b7316c5c72e880b0"
|
||||
content-hash = "19151bbc858317aec5747a8f45a86b47cc198111422cc166a94634ad1941d8bc"
|
||||
|
||||
[metadata.files]
|
||||
aiosqlite = [
|
||||
{file = "aiosqlite-0.17.0-py3-none-any.whl", hash = "sha256:6c49dc6d3405929b1d08eeccc72306d3677503cc5e5e43771efc1e00232e8231"},
|
||||
{file = "aiosqlite-0.17.0.tar.gz", hash = "sha256:f0e6acc24bc4864149267ac82fb46dfb3be4455f99fe21df82609cc6e6baee51"},
|
||||
]
|
||||
alembic = [
|
||||
{file = "alembic-1.8.0-py3-none-any.whl", hash = "sha256:b5ae4bbfc7d1302ed413989d39474d102e7cfa158f6d5969d2497955ffe85a30"},
|
||||
{file = "alembic-1.8.0.tar.gz", hash = "sha256:a2d4d90da70b30e70352cd9455e35873a255a31402a438fe24815758d7a0e5e1"},
|
||||
|
@ -1846,46 +1859,46 @@ soupsieve = [
|
|||
{file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"},
|
||||
]
|
||||
sqlalchemy = [
|
||||
{file = "SQLAlchemy-1.4.37-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:d9050b0c4a7f5538650c74aaba5c80cd64450e41c206f43ea6d194ae6d060ff9"},
|
||||
{file = "SQLAlchemy-1.4.37-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b4c92823889cf9846b972ee6db30c0e3a92c0ddfc76c6060a6cda467aa5fb694"},
|
||||
{file = "SQLAlchemy-1.4.37-cp27-cp27m-win32.whl", hash = "sha256:b55932fd0e81b43f4aff397c8ad0b3c038f540af37930423ab8f47a20b117e4c"},
|
||||
{file = "SQLAlchemy-1.4.37-cp27-cp27m-win_amd64.whl", hash = "sha256:4a17c1a1152ca4c29d992714aa9df3054da3af1598e02134f2e7314a32ef69d8"},
|
||||
{file = "SQLAlchemy-1.4.37-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ffe487570f47536b96eff5ef2b84034a8ba4e19aab5ab7647e677d94a119ea55"},
|
||||
{file = "SQLAlchemy-1.4.37-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:78363f400fbda80f866e8e91d37d36fe6313ff847ded08674e272873c1377ea5"},
|
||||
{file = "SQLAlchemy-1.4.37-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ee34c85cbda7779d66abac392c306ec78c13f5c73a1f01b8b767916d4895d23"},
|
||||
{file = "SQLAlchemy-1.4.37-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b38e088659b30c2ca0af63e5d139fad1779a7925d75075a08717a21c406c0f6"},
|
||||
{file = "SQLAlchemy-1.4.37-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6629c79967a6c92e33fad811599adf9bc5cee6e504a1027bbf9cc1b6fb2d276d"},
|
||||
{file = "SQLAlchemy-1.4.37-cp310-cp310-win32.whl", hash = "sha256:2aac2a685feb9882d09f457f4e5586c885d578af4e97a2b759e91e8c457cbce5"},
|
||||
{file = "SQLAlchemy-1.4.37-cp310-cp310-win_amd64.whl", hash = "sha256:7a44683cf97744a405103ef8fdd31199e9d7fc41b4a67e9044523b29541662b0"},
|
||||
{file = "SQLAlchemy-1.4.37-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:cffc67cdd07f0e109a1fc83e333972ae423ea5ad414585b63275b66b870ea62b"},
|
||||
{file = "SQLAlchemy-1.4.37-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17417327b87a0f703c9a20180f75e953315207d048159aff51822052f3e33e69"},
|
||||
{file = "SQLAlchemy-1.4.37-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:aaa0e90e527066409c2ea5676282cf4afb4a40bb9dce0f56c8ec2768bff22a6e"},
|
||||
{file = "SQLAlchemy-1.4.37-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1d9fb3931e27d59166bb5c4dcc911400fee51082cfba66ceb19ac954ade068"},
|
||||
{file = "SQLAlchemy-1.4.37-cp36-cp36m-win32.whl", hash = "sha256:0e7fd52e48e933771f177c2a1a484b06ea03774fc7741651ebdf19985a34037c"},
|
||||
{file = "SQLAlchemy-1.4.37-cp36-cp36m-win_amd64.whl", hash = "sha256:eec39a17bab3f69c44c9df4e0ed87c7306f2d2bf1eca3070af644927ec4199fa"},
|
||||
{file = "SQLAlchemy-1.4.37-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:caca6acf3f90893d7712ae2c6616ecfeac3581b4cc677c928a330ce6fbad4319"},
|
||||
{file = "SQLAlchemy-1.4.37-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50c8eaf44c3fed5ba6758d375de25f163e46137c39fda3a72b9ee1d1bb327dfc"},
|
||||
{file = "SQLAlchemy-1.4.37-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:139c50b9384e6d32a74fc4dcd0e9717f343ed38f95dbacf832c782c68e3862f3"},
|
||||
{file = "SQLAlchemy-1.4.37-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4c3b009c9220ae6e33f17b45f43fb46b9a1d281d76118405af13e26376f2e11"},
|
||||
{file = "SQLAlchemy-1.4.37-cp37-cp37m-win32.whl", hash = "sha256:9785d6f962d2c925aeb06a7539ac9d16608877da6aeaaf341984b3693ae80a02"},
|
||||
{file = "SQLAlchemy-1.4.37-cp37-cp37m-win_amd64.whl", hash = "sha256:3197441772dc3b1c6419f13304402f2418a18d7fe78000aa5a026e7100836739"},
|
||||
{file = "SQLAlchemy-1.4.37-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:3862a069a24f354145e01a76c7c720c263d62405fe5bed038c46a7ce900f5dd6"},
|
||||
{file = "SQLAlchemy-1.4.37-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e8706919829d455a9fa687c6bbd1b048e36fec3919a59f2d366247c2bfdbd9c"},
|
||||
{file = "SQLAlchemy-1.4.37-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:06ec11a5e6a4b6428167d3ce33b5bd455c020c867dabe3e6951fa98836e0741d"},
|
||||
{file = "SQLAlchemy-1.4.37-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d58f2d9d1a4b1459e8956a0153a4119da80f54ee5a9ea623cd568e99459a3ef1"},
|
||||
{file = "SQLAlchemy-1.4.37-cp38-cp38-win32.whl", hash = "sha256:d6927c9e3965b194acf75c8e0fb270b4d54512db171f65faae15ef418721996e"},
|
||||
{file = "SQLAlchemy-1.4.37-cp38-cp38-win_amd64.whl", hash = "sha256:a91d0668cada27352432f15b92ac3d43e34d8f30973fa8b86f5e9fddee928f3b"},
|
||||
{file = "SQLAlchemy-1.4.37-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:f9940528bf9c4df9e3c3872d23078b6b2da6431c19565637c09f1b88a427a684"},
|
||||
{file = "SQLAlchemy-1.4.37-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29a742c29fea12259f1d2a9ee2eb7fe4694a85d904a4ac66d15e01177b17ad7f"},
|
||||
{file = "SQLAlchemy-1.4.37-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7e579d6e281cc937bdb59917017ab98e618502067e04efb1d24ac168925e1d2a"},
|
||||
{file = "SQLAlchemy-1.4.37-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a940c551cfbd2e1e646ceea2777944425f5c3edff914bc808fe734d9e66f8d71"},
|
||||
{file = "SQLAlchemy-1.4.37-cp39-cp39-win32.whl", hash = "sha256:5e4e517ce72fad35cce364a01aff165f524449e9c959f1837dc71088afa2824c"},
|
||||
{file = "SQLAlchemy-1.4.37-cp39-cp39-win_amd64.whl", hash = "sha256:c37885f83b59e248bebe2b35beabfbea398cb40960cdc6d3a76eac863d4e1938"},
|
||||
{file = "SQLAlchemy-1.4.37.tar.gz", hash = "sha256:3688f92c62db6c5df268e2264891078f17ecb91e3141b400f2e28d0f75796dea"},
|
||||
{file = "SQLAlchemy-1.4.39-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:4770eb3ba69ec5fa41c681a75e53e0e342ac24c1f9220d883458b5596888e43a"},
|
||||
{file = "SQLAlchemy-1.4.39-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:752ef2e8dbaa3c5d419f322e3632f00ba6b1c3230f65bc97c2ff5c5c6c08f441"},
|
||||
{file = "SQLAlchemy-1.4.39-cp27-cp27m-win32.whl", hash = "sha256:b30e70f1594ee3c8902978fd71900d7312453922827c4ce0012fa6a8278d6df4"},
|
||||
{file = "SQLAlchemy-1.4.39-cp27-cp27m-win_amd64.whl", hash = "sha256:864d4f89f054819cb95e93100b7d251e4d114d1c60bc7576db07b046432af280"},
|
||||
{file = "SQLAlchemy-1.4.39-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8f901be74f00a13bf375241a778455ee864c2c21c79154aad196b7a994e1144f"},
|
||||
{file = "SQLAlchemy-1.4.39-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:1745987ada1890b0e7978abdb22c133eca2e89ab98dc17939042240063e1ef21"},
|
||||
{file = "SQLAlchemy-1.4.39-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ede13a472caa85a13abe5095e71676af985d7690eaa8461aeac5c74f6600b6c0"},
|
||||
{file = "SQLAlchemy-1.4.39-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7f13644b15665f7322f9e0635129e0ef2098409484df67fcd225d954c5861559"},
|
||||
{file = "SQLAlchemy-1.4.39-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26146c59576dfe9c546c9f45397a7c7c4a90c25679492ff610a7500afc7d03a6"},
|
||||
{file = "SQLAlchemy-1.4.39-cp310-cp310-win32.whl", hash = "sha256:91d2b89bb0c302f89e753bea008936acfa4e18c156fb264fe41eb6bbb2bbcdeb"},
|
||||
{file = "SQLAlchemy-1.4.39-cp310-cp310-win_amd64.whl", hash = "sha256:50e7569637e2e02253295527ff34666706dbb2bc5f6c61a5a7f44b9610c9bb09"},
|
||||
{file = "SQLAlchemy-1.4.39-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:107df519eb33d7f8e0d0d052128af2f25066c1a0f6b648fd1a9612ab66800b86"},
|
||||
{file = "SQLAlchemy-1.4.39-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f24d4d6ec301688c59b0c4bb1c1c94c5d0bff4ecad33bb8f5d9efdfb8d8bc925"},
|
||||
{file = "SQLAlchemy-1.4.39-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7b2785dd2a0c044a36836857ac27310dc7a99166253551ee8f5408930958cc60"},
|
||||
{file = "SQLAlchemy-1.4.39-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6e2c8581c6620136b9530137954a8376efffd57fe19802182c7561b0ab48b48"},
|
||||
{file = "SQLAlchemy-1.4.39-cp36-cp36m-win32.whl", hash = "sha256:fbc076f79d830ae4c9d49926180a1140b49fa675d0f0d555b44c9a15b29f4c80"},
|
||||
{file = "SQLAlchemy-1.4.39-cp36-cp36m-win_amd64.whl", hash = "sha256:0ec54460475f0c42512895c99c63d90dd2d9cbd0c13491a184182e85074b04c5"},
|
||||
{file = "SQLAlchemy-1.4.39-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:6f95706da857e6e79b54c33c1214f5467aab10600aa508ddd1239d5df271986e"},
|
||||
{file = "SQLAlchemy-1.4.39-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:621f050e72cc7dfd9ad4594ff0abeaad954d6e4a2891545e8f1a53dcdfbef445"},
|
||||
{file = "SQLAlchemy-1.4.39-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05a05771617bfa723ba4cef58d5b25ac028b0d68f28f403edebed5b8243b3a87"},
|
||||
{file = "SQLAlchemy-1.4.39-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20bf65bcce65c538e68d5df27402b39341fabeecf01de7e0e72b9d9836c13c52"},
|
||||
{file = "SQLAlchemy-1.4.39-cp37-cp37m-win32.whl", hash = "sha256:f2a42acc01568b9701665e85562bbff78ec3e21981c7d51d56717c22e5d3d58b"},
|
||||
{file = "SQLAlchemy-1.4.39-cp37-cp37m-win_amd64.whl", hash = "sha256:6d81de54e45f1d756785405c9d06cd17918c2eecc2d4262dc2d276ca612c2f61"},
|
||||
{file = "SQLAlchemy-1.4.39-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:5c2d19bfb33262bf987ef0062345efd0f54c4189c2d95159c72995457bf4a359"},
|
||||
{file = "SQLAlchemy-1.4.39-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14ea8ff2d33c48f8e6c3c472111d893b9e356284d1482102da9678195e5a8eac"},
|
||||
{file = "SQLAlchemy-1.4.39-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec3985c883d6d217cf2013028afc6e3c82b8907192ba6195d6e49885bfc4b19d"},
|
||||
{file = "SQLAlchemy-1.4.39-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1962dfee37b7fb17d3d4889bf84c4ea08b1c36707194c578f61e6e06d12ab90f"},
|
||||
{file = "SQLAlchemy-1.4.39-cp38-cp38-win32.whl", hash = "sha256:047ef5ccd8860f6147b8ac6c45a4bc573d4e030267b45d9a1c47b55962ff0e6f"},
|
||||
{file = "SQLAlchemy-1.4.39-cp38-cp38-win_amd64.whl", hash = "sha256:b71be98ef6e180217d1797185c75507060a57ab9cd835653e0112db16a710f0d"},
|
||||
{file = "SQLAlchemy-1.4.39-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:365b75938049ae31cf2176efd3d598213ddb9eb883fbc82086efa019a5f649df"},
|
||||
{file = "SQLAlchemy-1.4.39-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7a7667d928ba6ee361a3176e1bef6847c1062b37726b33505cc84136f657e0d"},
|
||||
{file = "SQLAlchemy-1.4.39-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c6d00cb9da8d0cbfaba18cad046e94b06de6d4d0ffd9d4095a3ad1838af22528"},
|
||||
{file = "SQLAlchemy-1.4.39-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0538b66f959771c56ff996d828081908a6a52a47c5548faed4a3d0a027a5368"},
|
||||
{file = "SQLAlchemy-1.4.39-cp39-cp39-win32.whl", hash = "sha256:d1f665e50592caf4cad3caed3ed86f93227bffe0680218ccbb293bd5a6734ca8"},
|
||||
{file = "SQLAlchemy-1.4.39-cp39-cp39-win_amd64.whl", hash = "sha256:8b773c9974c272aae0fa7e95b576d98d17ee65f69d8644f9b6ffc90ee96b4d19"},
|
||||
{file = "SQLAlchemy-1.4.39.tar.gz", hash = "sha256:8194896038753b46b08a0b0ae89a5d80c897fb601dd51e243ed5720f1f155d27"},
|
||||
]
|
||||
sqlalchemy2-stubs = [
|
||||
{file = "sqlalchemy2-stubs-0.0.2a23.tar.gz", hash = "sha256:a13d94e23b5b0da8ee21986ef8890788a1f2eb26c2a9f39424cc933e4e7e87ff"},
|
||||
{file = "sqlalchemy2_stubs-0.0.2a23-py3-none-any.whl", hash = "sha256:6011d2219365d4e51f3e9d83ffeb5b904964ef1d143dc1298d8a70ce8641014d"},
|
||||
{file = "sqlalchemy2-stubs-0.0.2a24.tar.gz", hash = "sha256:e15c45302eafe196ed516f979ef017135fd619d2c62d02de9a5c5f2e59a600c4"},
|
||||
{file = "sqlalchemy2_stubs-0.0.2a24-py3-none-any.whl", hash = "sha256:f2399251d3d8f00a88659d711a449c855a0d4e977c7a9134e414f1459b9acc11"},
|
||||
]
|
||||
starlette = [
|
||||
{file = "starlette-0.19.1-py3-none-any.whl", hash = "sha256:5a60c5c2d051f3a8eb546136aa0c9399773a689595e099e0877704d5888279bf"},
|
||||
|
|
|
@ -17,7 +17,7 @@ python-multipart = "^0.0.5"
|
|||
tomli = "^2.0.1"
|
||||
httpx = "^0.23.0"
|
||||
timeago = "^1.0.15"
|
||||
SQLAlchemy = {extras = ["mypy"], version = "^1.4.37"}
|
||||
SQLAlchemy = {extras = ["asyncio"], version = "^1.4.39"}
|
||||
alembic = "^1.8.0"
|
||||
bleach = "^5.0.0"
|
||||
requests = "^2.27.1"
|
||||
|
@ -38,6 +38,8 @@ html2text = "^2020.1.16"
|
|||
feedgen = "^0.9.0"
|
||||
emoji = "^1.7.0"
|
||||
PyLD = "^2.0.3"
|
||||
aiosqlite = "^0.17.0"
|
||||
sqlalchemy2-stubs = "^0.0.2-alpha.24"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
black = "^22.3.0"
|
||||
|
|
|
@ -2,22 +2,25 @@ from typing import Generator
|
|||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import orm
|
||||
|
||||
from app.database import Base
|
||||
from app.database import async_engine
|
||||
from app.database import async_session
|
||||
from app.database import engine
|
||||
from app.database import get_db
|
||||
from app.main import app
|
||||
from tests.factories import _Session
|
||||
|
||||
# _Session = orm.sessionmaker(bind=engine, autocommit=False, autoflush=False)
|
||||
|
||||
|
||||
def _get_db_for_testing() -> Generator[orm.Session, None, None]:
|
||||
# try:
|
||||
yield _Session # type: ignore
|
||||
# finally:
|
||||
# session.close()
|
||||
@pytest.fixture
|
||||
async def async_db_session():
|
||||
async with async_session() as session:
|
||||
async with async_engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
yield session
|
||||
async with async_engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -46,6 +49,6 @@ def exclude_fastapi_middleware():
|
|||
|
||||
@pytest.fixture
|
||||
def client(db, exclude_fastapi_middleware) -> Generator:
|
||||
app.dependency_overrides[get_db] = _get_db_for_testing
|
||||
# app.dependency_overrides[get_db] = _get_db_for_testing
|
||||
with TestClient(app) as c:
|
||||
yield c
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
import httpx
|
||||
import pytest
|
||||
import respx
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import models
|
||||
from app.actor import fetch_actor
|
||||
from app.database import Session
|
||||
from app.database import AsyncSession
|
||||
from tests import factories
|
||||
|
||||
|
||||
def test_fetch_actor(db: Session, respx_mock) -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_actor(async_db_session: AsyncSession, respx_mock) -> None:
|
||||
# Given a remote actor
|
||||
ra = factories.RemoteActorFactory(
|
||||
base_url="https://example.com",
|
||||
|
@ -17,18 +22,22 @@ def test_fetch_actor(db: Session, respx_mock) -> None:
|
|||
respx_mock.get(ra.ap_id).mock(return_value=httpx.Response(200, json=ra.ap_actor))
|
||||
|
||||
# When fetching this actor for the first time
|
||||
saved_actor = fetch_actor(db, ra.ap_id)
|
||||
saved_actor = await fetch_actor(async_db_session, ra.ap_id)
|
||||
|
||||
# Then it has been fetched and saved in DB
|
||||
assert respx.calls.call_count == 1
|
||||
assert db.query(models.Actor).one().ap_id == saved_actor.ap_id
|
||||
assert (
|
||||
await async_db_session.execute(select(models.Actor))
|
||||
).scalar_one().ap_id == saved_actor.ap_id
|
||||
|
||||
# When fetching it a second time
|
||||
actor_from_db = fetch_actor(db, ra.ap_id)
|
||||
actor_from_db = await fetch_actor(async_db_session, ra.ap_id)
|
||||
|
||||
# Then it's read from the DB
|
||||
assert actor_from_db.ap_id == ra.ap_id
|
||||
assert db.query(models.Actor).count() == 1
|
||||
assert (
|
||||
await async_db_session.execute(select(func.count(models.Actor.id)))
|
||||
).scalar_one() == 1
|
||||
assert respx.calls.call_count == 1
|
||||
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import activitypub as ap
|
||||
from app import models
|
||||
from app.config import generate_csrf_token
|
||||
from app.database import Session
|
||||
from app.utils.emoji import EMOJIS_BY_NAME
|
||||
from tests.utils import generate_admin_session_cookies
|
||||
|
||||
|
|
|
@ -3,12 +3,12 @@ from uuid import uuid4
|
|||
import httpx
|
||||
import respx
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import activitypub as ap
|
||||
from app import models
|
||||
from app.actor import LOCAL_ACTOR
|
||||
from app.ap_object import RemoteObject
|
||||
from app.database import Session
|
||||
from tests import factories
|
||||
from tests.utils import mock_httpsig_checker
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ from uuid import uuid4
|
|||
import httpx
|
||||
import respx
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import activitypub as ap
|
||||
from app import models
|
||||
|
@ -11,7 +12,6 @@ from app import webfinger
|
|||
from app.actor import LOCAL_ACTOR
|
||||
from app.ap_object import RemoteObject
|
||||
from app.config import generate_csrf_token
|
||||
from app.database import Session
|
||||
from tests import factories
|
||||
from tests.utils import generate_admin_session_cookies
|
||||
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
from uuid import uuid4
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
import respx
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import models
|
||||
from app.actor import LOCAL_ACTOR
|
||||
from app.ap_object import RemoteObject
|
||||
from app.database import Session
|
||||
from app.database import AsyncSession
|
||||
from app.outgoing_activities import _MAX_RETRIES
|
||||
from app.outgoing_activities import new_outgoing_activity
|
||||
from app.outgoing_activities import process_next_outgoing_activity
|
||||
|
@ -36,8 +39,9 @@ def _setup_outbox_object() -> models.OutboxObject:
|
|||
return outbox_object
|
||||
|
||||
|
||||
def test_new_outgoing_activity(
|
||||
db: Session,
|
||||
@pytest.mark.asyncio
|
||||
async def test_new_outgoing_activity(
|
||||
async_db_session: AsyncSession,
|
||||
client: TestClient,
|
||||
respx_mock: respx.MockRouter,
|
||||
) -> None:
|
||||
|
@ -48,9 +52,13 @@ def test_new_outgoing_activity(
|
|||
raise ValueError("Should never happen")
|
||||
|
||||
# When queuing the activity
|
||||
outgoing_activity = new_outgoing_activity(db, inbox_url, outbox_object.id)
|
||||
outgoing_activity = await new_outgoing_activity(
|
||||
async_db_session, inbox_url, outbox_object.id
|
||||
)
|
||||
|
||||
assert db.query(models.OutgoingActivity).one() == outgoing_activity
|
||||
assert (
|
||||
await async_db_session.execute(select(models.OutgoingActivity))
|
||||
).scalar_one() == outgoing_activity
|
||||
assert outgoing_activity.outbox_object_id == outbox_object.id
|
||||
assert outgoing_activity.recipient == inbox_url
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import activitypub as ap
|
||||
from app.actor import LOCAL_ACTOR
|
||||
from app.database import Session
|
||||
|
||||
_ACCEPTED_AP_HEADERS = [
|
||||
"application/activity+json",
|
||||
|
|
Loading…
Reference in a new issue