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