2023-03-17 10:59:29 +01:00
|
|
|
#!/usr/bin/env python3
|
2023-03-18 13:02:38 +01:00
|
|
|
from typing import Any
|
|
|
|
import uuid
|
|
|
|
|
2023-03-20 10:54:54 +01:00
|
|
|
from sqlalchemy.orm import session
|
|
|
|
|
2023-03-17 10:59:29 +01:00
|
|
|
from app import models
|
2023-04-01 18:13:16 +02:00
|
|
|
from app import ldsig
|
2023-03-17 10:59:29 +01:00
|
|
|
from app.database import AsyncSession
|
2023-03-22 10:58:11 +01:00
|
|
|
from app.models import InboxObject, OutboxObject, now
|
2023-03-17 18:46:31 +01:00
|
|
|
from app.activitypub import ME
|
2023-03-20 10:54:54 +01:00
|
|
|
from app.activitypub import handle_visibility
|
2023-03-17 18:46:31 +01:00
|
|
|
from app.config import MANUALLY_APPROVES_FOLLOWERS
|
2023-04-01 18:13:16 +02:00
|
|
|
from app.config import BASE_URL, ID
|
2023-03-18 13:02:38 +01:00
|
|
|
from app.models import Actor
|
|
|
|
from app.actor import fetch_actor
|
2023-04-01 18:13:16 +02:00
|
|
|
from app.httpsig import k
|
2023-03-17 10:59:29 +01:00
|
|
|
|
2023-03-17 18:46:31 +01:00
|
|
|
import app.activitypub as ap
|
2023-03-18 13:02:38 +01:00
|
|
|
|
2023-03-20 10:54:54 +01:00
|
|
|
from urllib.parse import urlparse
|
2023-03-18 13:02:38 +01:00
|
|
|
from sqlalchemy import select
|
2023-03-20 16:33:10 +01:00
|
|
|
from sqlalchemy import delete
|
|
|
|
from sqlalchemy.orm import joinedload
|
2023-03-20 10:54:54 +01:00
|
|
|
from sqlalchemy.exc import IntegrityError
|
2023-03-18 13:02:38 +01:00
|
|
|
from loguru import logger
|
|
|
|
from uuid import uuid4
|
2023-04-01 18:13:16 +02:00
|
|
|
from datetime import datetime
|
2023-03-18 13:02:38 +01:00
|
|
|
|
2023-03-17 18:46:31 +01:00
|
|
|
|
|
|
|
|
|
|
|
def allocate_outbox_id() -> str:
|
|
|
|
return str(uuid.uuid4())
|
|
|
|
|
|
|
|
|
|
|
|
def build_object_id(id) -> str:
|
|
|
|
return f"{BASE_URL}/tail/{id}"
|
2023-03-17 10:59:29 +01:00
|
|
|
|
2023-03-20 10:54:54 +01:00
|
|
|
|
2023-03-17 10:59:29 +01:00
|
|
|
async def save_incoming(
|
|
|
|
db_session: AsyncSession,
|
|
|
|
payload: dict,
|
|
|
|
) -> models.IncomingActivity | None:
|
|
|
|
ap_id: str
|
|
|
|
if "@context" not in payload:
|
|
|
|
logger.warning(f"invalid object: {payload}")
|
|
|
|
return None
|
|
|
|
|
|
|
|
if "id" in payload:
|
|
|
|
ap_id = payload["id"]
|
|
|
|
else:
|
|
|
|
ap_id = str(uuid4())
|
|
|
|
|
|
|
|
incoming_activity = models.IncomingActivity(
|
|
|
|
ap_id=ap_id,
|
|
|
|
ap_object=payload,
|
|
|
|
)
|
2023-03-20 16:33:10 +01:00
|
|
|
|
2023-03-21 02:32:26 +01:00
|
|
|
if await process_incoming(db_session, payload):
|
|
|
|
await db_session.commit()
|
|
|
|
await db_session.flush()
|
2023-03-18 09:12:44 +01:00
|
|
|
return incoming_activity
|
|
|
|
|
2023-03-20 16:33:10 +01:00
|
|
|
db_session.add(incoming_activity)
|
2023-03-17 10:59:29 +01:00
|
|
|
await db_session.commit()
|
|
|
|
await db_session.refresh(incoming_activity)
|
|
|
|
return incoming_activity
|
2023-03-17 18:46:31 +01:00
|
|
|
|
|
|
|
|
|
|
|
async def process_incoming(
|
|
|
|
db_session: AsyncSession,
|
|
|
|
ap_object: dict,
|
|
|
|
) -> bool:
|
2023-03-21 06:58:14 +01:00
|
|
|
actor = await fetch_actor(db_session, ap_object["actor"])
|
2023-03-17 18:46:31 +01:00
|
|
|
|
2023-03-27 12:18:04 +02:00
|
|
|
def build_object(
|
|
|
|
object,
|
|
|
|
relates_to_inbox_object_id = None,
|
|
|
|
relates_to_outbox_object_id = None,
|
|
|
|
) -> InboxObject:
|
2023-03-20 10:54:54 +01:00
|
|
|
inbox_object = models.InboxObject(
|
|
|
|
actor_id=actor.id,
|
|
|
|
server=urlparse(object["id"]).hostname,
|
|
|
|
is_hidden_from_stream=True,
|
|
|
|
ap_actor_id=actor.ap_id,
|
|
|
|
ap_type=object["type"],
|
|
|
|
ap_id=object["id"],
|
|
|
|
ap_published_at=now(),
|
|
|
|
ap_object=object,
|
|
|
|
visibility=handle_visibility(object),
|
2023-03-27 12:18:04 +02:00
|
|
|
activity_object_ap_id=object["actor"],
|
|
|
|
relates_to_inbox_object_id=relates_to_inbox_object_id,
|
|
|
|
relates_to_outbox_object_id=relates_to_outbox_object_id,
|
2023-03-20 10:54:54 +01:00
|
|
|
)
|
|
|
|
return inbox_object
|
|
|
|
|
2023-03-17 18:46:31 +01:00
|
|
|
if "Follow" == ap_object["type"]:
|
2023-03-20 16:33:10 +01:00
|
|
|
inbox_object = build_object(ap_object)
|
2023-03-20 10:54:54 +01:00
|
|
|
db_session.add(inbox_object)
|
|
|
|
await db_session.flush()
|
|
|
|
await db_session.refresh(inbox_object)
|
|
|
|
|
|
|
|
if await _handle_follow(db_session, actor, inbox_object):
|
2023-03-18 15:55:49 +01:00
|
|
|
return True
|
|
|
|
return False
|
2023-03-20 16:33:10 +01:00
|
|
|
elif "Undo" == ap_object["type"]:
|
|
|
|
#inbox_object = build_object(ap_object)
|
|
|
|
#db_session.add(inbox_object)
|
|
|
|
if await _handle_undo(db_session, ap_object):
|
|
|
|
return True
|
2023-03-27 12:18:04 +02:00
|
|
|
elif ap_object["type"] in ["Accept", "Rejact"]:
|
|
|
|
relate_following_object = (await db_session.execute(
|
|
|
|
select(models.OutboxObject)
|
|
|
|
.where(models.OutboxObject.ap_id == ap_object["object"])
|
|
|
|
.options(
|
|
|
|
joinedload(models.OutboxObject.relates_to_inbox_object).options(
|
|
|
|
joinedload(models.InboxObject.actor)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
).unique().scalar_one_or_none()
|
|
|
|
|
|
|
|
if "Accept" == ap_object["type"]:
|
|
|
|
inbox_object = build_object(ap_object)
|
|
|
|
db_session.add(inbox_object)
|
|
|
|
await db_session.flush()
|
|
|
|
await db_session.refresh(inbox_object)
|
|
|
|
|
|
|
|
following = models.Following(
|
|
|
|
actor_id=relate_following_object.relates_to_actor.id, #type: ignore
|
|
|
|
outbox_object_id=relate_following_object.id, #type: ignore
|
|
|
|
ap_actor_id=ap_object["actor"], #type: ignore
|
|
|
|
)
|
|
|
|
|
|
|
|
db_session.add(following)
|
|
|
|
await db_session.flush()
|
|
|
|
await db_session.refresh(following)
|
2023-03-27 17:22:11 +02:00
|
|
|
return True
|
2023-04-01 18:13:16 +02:00
|
|
|
# elif "Creat" == ap_object["type"]:
|
|
|
|
|
2023-03-18 09:12:44 +01:00
|
|
|
|
|
|
|
return False
|
2023-03-17 18:46:31 +01:00
|
|
|
|
|
|
|
|
|
|
|
async def _handle_follow(
|
2023-03-18 13:02:38 +01:00
|
|
|
db_session : AsyncSession,
|
2023-03-20 10:54:54 +01:00
|
|
|
actor : Actor,
|
|
|
|
inbox_object : InboxObject,
|
2023-03-18 09:12:44 +01:00
|
|
|
) -> bool:
|
2023-03-20 10:54:54 +01:00
|
|
|
if ME["id"] != inbox_object.ap_object["object"]: #type: ignore
|
2023-03-17 18:46:31 +01:00
|
|
|
# await db_session.delete(ap_object)
|
2023-03-20 10:54:54 +01:00
|
|
|
logger.warning("no match follow object!" + inbox_object.ap_object["id"]) #type: ignore
|
2023-03-18 09:12:44 +01:00
|
|
|
return False
|
2023-03-17 18:46:31 +01:00
|
|
|
|
|
|
|
if MANUALLY_APPROVES_FOLLOWERS:
|
|
|
|
# TODO
|
2023-03-18 09:12:44 +01:00
|
|
|
return False
|
2023-03-17 18:46:31 +01:00
|
|
|
|
2023-03-20 10:54:54 +01:00
|
|
|
await _send_accept(db_session, actor, inbox_object)
|
2023-03-18 09:12:44 +01:00
|
|
|
return True
|
2023-03-17 18:46:31 +01:00
|
|
|
|
|
|
|
|
|
|
|
async def _send_accept(
|
|
|
|
db_session: AsyncSession,
|
2023-03-20 10:54:54 +01:00
|
|
|
actor : Actor,
|
|
|
|
inbox_object : InboxObject,
|
2023-03-17 18:46:31 +01:00
|
|
|
) -> None :
|
2023-03-20 10:54:54 +01:00
|
|
|
follower = models.Follower(
|
|
|
|
actor_id=inbox_object.actor_id,
|
|
|
|
inbox_object_id=inbox_object.id,
|
|
|
|
ap_actor_id=inbox_object.ap_object["actor"], #type: ignore
|
|
|
|
)
|
|
|
|
|
|
|
|
try:
|
|
|
|
db_session.add(follower)
|
|
|
|
await db_session.flush()
|
|
|
|
except IntegrityError:
|
|
|
|
await db_session.rollback()
|
|
|
|
logger.warning("existing follower in db!")
|
2023-03-17 18:46:31 +01:00
|
|
|
|
2023-04-01 18:13:16 +02:00
|
|
|
try:
|
|
|
|
reply_id = allocate_outbox_id()
|
|
|
|
|
|
|
|
url = actor.inbox_url # type: ignore
|
|
|
|
out = {
|
|
|
|
"@context": ap.AS_CTX,
|
|
|
|
"id": build_object_id(reply_id),
|
|
|
|
"type": "Accept",
|
|
|
|
"actor": ME["id"],
|
|
|
|
"object": inbox_object.ap_object["id"], #type: ignore
|
|
|
|
}
|
|
|
|
#TODO outcoming
|
|
|
|
await ap.post(url, out) # type: ignore
|
|
|
|
except Exception as e:
|
|
|
|
logger.error(e)
|
2023-03-20 16:33:10 +01:00
|
|
|
|
|
|
|
|
|
|
|
async def _handle_undo(
|
|
|
|
db_session : AsyncSession,
|
|
|
|
inbox_object : dict
|
|
|
|
) -> bool:
|
|
|
|
if inbox_object["object"]["object"] != ME["id"]:
|
|
|
|
logger.warning("Wrong undo object! "
|
|
|
|
+ inbox_object["object"]["actor"])
|
|
|
|
return False
|
|
|
|
|
|
|
|
if "Follow" == inbox_object["object"]["type"]:
|
|
|
|
relate_object = (await db_session.execute(
|
|
|
|
select(models.InboxObject)
|
|
|
|
.where(models.InboxObject.ap_id == inbox_object["object"]["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
|
|
|
|
|
|
|
|
if relate_object:
|
|
|
|
relate_object.undo_id=inbox_object["object"]["id"]
|
|
|
|
relate_object.is_deleted=True
|
|
|
|
|
|
|
|
await db_session.execute(
|
|
|
|
delete(models.Follower).where(
|
|
|
|
models.Follower.ap_actor_id == inbox_object["actor"]
|
|
|
|
)
|
|
|
|
)
|
|
|
|
logger.info("undo follow " + inbox_object["actor"])
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
2023-03-21 06:58:14 +01:00
|
|
|
|
|
|
|
|
|
|
|
async def send_follow(
|
|
|
|
db_session : AsyncSession,
|
|
|
|
acct : str
|
|
|
|
):
|
|
|
|
await _send_follow(db_session, acct)
|
|
|
|
await db_session.commit()
|
|
|
|
await db_session.flush()
|
|
|
|
|
2023-03-22 10:58:11 +01:00
|
|
|
|
2023-03-21 06:58:14 +01:00
|
|
|
async def _send_follow(
|
|
|
|
db_session : AsyncSession,
|
|
|
|
actor_url : str,
|
|
|
|
):
|
|
|
|
actor = await fetch_actor(db_session, actor_url)
|
|
|
|
|
2023-03-27 12:18:04 +02:00
|
|
|
follow_id = build_object_id(allocate_outbox_id())
|
2023-03-21 06:58:14 +01:00
|
|
|
out = {
|
|
|
|
"@context": ap.AS_CTX,
|
2023-03-27 12:18:04 +02:00
|
|
|
"id": follow_id,
|
2023-03-21 06:58:14 +01:00
|
|
|
"type": "Follow",
|
|
|
|
"actor": ME["id"],
|
|
|
|
"object": actor.ap_id,
|
|
|
|
}
|
2023-03-22 10:58:11 +01:00
|
|
|
|
2023-03-27 12:18:04 +02:00
|
|
|
await save_to_outbox(
|
|
|
|
db_session,
|
|
|
|
follow_id,
|
|
|
|
out,
|
|
|
|
relates_to_actor_id=actor.id, # type: ignore
|
|
|
|
)
|
2023-03-22 10:58:11 +01:00
|
|
|
|
2023-03-21 06:58:14 +01:00
|
|
|
await ap.post(
|
2023-03-21 07:24:57 +01:00
|
|
|
actor.inbox_url,
|
2023-03-21 06:58:14 +01:00
|
|
|
out,
|
|
|
|
)
|
2023-03-22 10:58:11 +01:00
|
|
|
|
|
|
|
|
2023-04-01 18:13:16 +02:00
|
|
|
async def _send_create(
|
|
|
|
db_session: AsyncSession,
|
|
|
|
ap_type: str,
|
|
|
|
content: str,
|
|
|
|
visibility: ap.VisibilityEnum,
|
|
|
|
published: str | None = None,
|
|
|
|
) -> bool:
|
2023-04-01 18:34:46 +02:00
|
|
|
object_id = allocate_outbox_id()
|
2023-04-01 18:13:16 +02:00
|
|
|
if not published:
|
|
|
|
published = now().replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
|
|
|
|
|
|
to = []
|
|
|
|
cc = []
|
|
|
|
|
|
|
|
if visibility == ap.VisibilityEnum.PUBLIC:
|
|
|
|
to = [ap.AS_PUBLIC]
|
|
|
|
cc = [f"{BASE_URL}/followers"]
|
|
|
|
else:
|
|
|
|
raise ValueError(f"Unsupport visibility {visibility}")
|
|
|
|
|
|
|
|
ap_object = {
|
|
|
|
"@context": ap.AS_EXTENDED_CTX,
|
|
|
|
"type": ap_type,
|
2023-04-01 18:34:46 +02:00
|
|
|
"id": build_object_id(object_id),
|
2023-04-01 18:13:16 +02:00
|
|
|
"attributedTo": ID,
|
|
|
|
"content": content,
|
|
|
|
"to": to,
|
|
|
|
"cc": cc,
|
|
|
|
"published": published,
|
|
|
|
# "context": context,
|
|
|
|
# "conversation": context,
|
2023-04-01 18:34:46 +02:00
|
|
|
"url": build_object_id(object_id),
|
2023-04-01 18:13:16 +02:00
|
|
|
"tag": [],
|
|
|
|
"summary": None,
|
|
|
|
"inReplyTo": None,
|
|
|
|
"sensitive": False,
|
|
|
|
"attachment": [],
|
|
|
|
}
|
|
|
|
|
|
|
|
outbox_object = await save_to_outbox(
|
|
|
|
db_session,
|
|
|
|
object_id,
|
|
|
|
ap_object,
|
|
|
|
)
|
|
|
|
|
|
|
|
recipients = await _compute_recipients(db_session, ap_object)
|
|
|
|
ap_object = ap.wrap_ap_object(ap_object)
|
|
|
|
|
|
|
|
if ap_object["type"] == "Create":
|
|
|
|
if ap.VisibilityEnum.PUBLIC == outbox_object.visibility:
|
|
|
|
ldsig.generate_signature(ap_object, k)
|
|
|
|
|
|
|
|
for r in recipients:
|
|
|
|
await ap.post(
|
|
|
|
r,
|
|
|
|
ap_object,
|
|
|
|
)
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
async def _compute_recipients(
|
|
|
|
db_session: AsyncSession,
|
|
|
|
ap_object: dict,
|
|
|
|
) -> set[str]:
|
|
|
|
|
|
|
|
async def process_collection(
|
|
|
|
db_session,
|
|
|
|
url) -> list[Actor]:
|
|
|
|
if url == BASE_URL + "/followers":
|
|
|
|
followers = (
|
|
|
|
(
|
|
|
|
await db_session.scalars(
|
|
|
|
select(models.Follower).options(
|
|
|
|
joinedload(models.Follower.actor)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
.unique()
|
|
|
|
.all()
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
raise ValueError(f"{url}) not supported")
|
|
|
|
|
|
|
|
return [follower.actor for follower in followers]
|
|
|
|
|
|
|
|
_recipients = []
|
|
|
|
for field in ["to", "cc", "bcc", "bto"]:
|
|
|
|
if field in ap_object:
|
|
|
|
_recipients.extend(ap_object[field])
|
|
|
|
|
|
|
|
recipients = set()
|
|
|
|
logger.info(f"{_recipients}")
|
|
|
|
for r in _recipients:
|
|
|
|
if r in [ap.AS_PUBLIC, ID]:
|
|
|
|
continue
|
|
|
|
|
|
|
|
if r.startswith(BASE_URL):
|
|
|
|
for actor in await process_collection(db_session, r):
|
|
|
|
recipients.add(actor.share_inbox_url)
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
return recipients
|
|
|
|
|
|
|
|
|
2023-03-27 12:18:04 +02:00
|
|
|
async def save_to_inbox(
|
|
|
|
db_session : AsyncSession,
|
|
|
|
inbox_id : str,
|
|
|
|
ap_object : dict,
|
|
|
|
relates_to_inbox_object_id: int | None = None,
|
|
|
|
relates_to_outbox_object_id: int | None = None,
|
|
|
|
relates_to_actor_id: int | None = None,
|
|
|
|
) -> InboxObject:
|
|
|
|
|
|
|
|
ap_type = ap_object["type"]
|
|
|
|
ap_id = ap_object["id"]
|
|
|
|
visibility = handle_visibility(ap_object)
|
|
|
|
|
|
|
|
inbox_object = OutboxObject(
|
|
|
|
public_id=inbox_id,
|
|
|
|
ap_object=ap_object,
|
|
|
|
ap_id=ap_id,
|
|
|
|
ap_type=ap_type,
|
|
|
|
visibility =visibility,
|
|
|
|
relates_to_inbox_object_id=relates_to_inbox_object_id,
|
|
|
|
relates_to_outbox_object_id=relates_to_outbox_object_id,
|
|
|
|
relates_to_actor_id=relates_to_actor_id,
|
|
|
|
)
|
|
|
|
|
|
|
|
db_session.add(inbox_object)
|
|
|
|
await db_session.flush()
|
|
|
|
await db_session.refresh(inbox_object)
|
|
|
|
|
|
|
|
return inbox_object
|
|
|
|
|
|
|
|
|
2023-03-22 10:58:11 +01:00
|
|
|
async def save_to_outbox(
|
|
|
|
db_session : AsyncSession,
|
|
|
|
outbox_id : str,
|
|
|
|
ap_object : dict,
|
|
|
|
relates_to_inbox_object_id: int | None = None,
|
|
|
|
relates_to_outbox_object_id: int | None = None,
|
|
|
|
relates_to_actor_id: int | None = None,
|
|
|
|
) -> OutboxObject:
|
2023-03-27 12:18:04 +02:00
|
|
|
ap_type = ap_object["type"]
|
|
|
|
ap_id = ap_object["id"]
|
|
|
|
visibility = handle_visibility(ap_object)
|
2023-03-22 10:58:11 +01:00
|
|
|
|
|
|
|
outbox_object = OutboxObject(
|
|
|
|
public_id=outbox_id,
|
|
|
|
ap_object=ap_object,
|
2023-03-27 12:18:04 +02:00
|
|
|
ap_id=ap_id,
|
|
|
|
ap_type=ap_type,
|
|
|
|
visibility =visibility,
|
2023-03-22 10:58:11 +01:00
|
|
|
relates_to_inbox_object_id=relates_to_inbox_object_id,
|
|
|
|
relates_to_outbox_object_id=relates_to_outbox_object_id,
|
|
|
|
relates_to_actor_id=relates_to_actor_id,
|
|
|
|
)
|
|
|
|
|
|
|
|
db_session.add(outbox_object)
|
|
|
|
await db_session.flush()
|
|
|
|
await db_session.refresh(outbox_object)
|
2023-04-01 18:40:03 +02:00
|
|
|
await db_session.commit()
|
2023-03-22 10:58:11 +01:00
|
|
|
|
|
|
|
return outbox_object
|