2022-06-22 18:11:22 +00:00
|
|
|
import hashlib
|
|
|
|
from datetime import datetime
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
import pydantic
|
|
|
|
from dateutil.parser import isoparse
|
|
|
|
from markdown import markdown
|
|
|
|
|
|
|
|
from app import activitypub as ap
|
|
|
|
from app.actor import LOCAL_ACTOR
|
|
|
|
from app.actor import Actor
|
|
|
|
from app.actor import RemoteActor
|
2022-06-25 06:23:28 +00:00
|
|
|
from app.media import proxied_media_url
|
2022-06-22 18:11:22 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Object:
|
|
|
|
@property
|
|
|
|
def is_from_db(self) -> bool:
|
|
|
|
return False
|
|
|
|
|
2022-06-25 08:20:07 +00:00
|
|
|
@property
|
|
|
|
def is_from_outbox(self) -> bool:
|
|
|
|
return False
|
|
|
|
|
|
|
|
@property
|
|
|
|
def is_from_inbox(self) -> bool:
|
|
|
|
return False
|
|
|
|
|
2022-06-22 18:11:22 +00:00
|
|
|
@property
|
|
|
|
def ap_type(self) -> str:
|
|
|
|
return self.ap_object["type"]
|
|
|
|
|
|
|
|
@property
|
|
|
|
def ap_object(self) -> ap.RawObject:
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
@property
|
|
|
|
def ap_id(self) -> str:
|
|
|
|
return ap.get_id(self.ap_object["id"])
|
|
|
|
|
|
|
|
@property
|
|
|
|
def ap_actor_id(self) -> str:
|
|
|
|
return ap.get_actor_id(self.ap_object)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def ap_published_at(self) -> datetime | None:
|
|
|
|
# TODO: default to None? or now()?
|
|
|
|
if "published" in self.ap_object:
|
|
|
|
return isoparse(self.ap_object["published"])
|
|
|
|
elif "created" in self.ap_object:
|
|
|
|
return isoparse(self.ap_object["created"])
|
|
|
|
return None
|
|
|
|
|
|
|
|
@property
|
|
|
|
def actor(self) -> Actor:
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
|
|
@property
|
|
|
|
def visibility(self) -> ap.VisibilityEnum:
|
2022-06-26 16:07:55 +00:00
|
|
|
return ap.object_visibility(self.ap_object, self.actor)
|
2022-06-22 18:11:22 +00:00
|
|
|
|
|
|
|
@property
|
2022-06-24 20:41:43 +00:00
|
|
|
def ap_context(self) -> str | None:
|
2022-06-24 09:33:05 +00:00
|
|
|
return self.ap_object.get("context") or self.ap_object.get("conversation")
|
2022-06-22 18:11:22 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def sensitive(self) -> bool:
|
|
|
|
return self.ap_object.get("sensitive", False)
|
|
|
|
|
2022-06-26 16:07:55 +00:00
|
|
|
@property
|
|
|
|
def tags(self) -> list[ap.RawObject]:
|
|
|
|
return self.ap_object.get("tag", [])
|
|
|
|
|
2022-06-22 18:11:22 +00:00
|
|
|
@property
|
2022-06-24 09:33:05 +00:00
|
|
|
def attachments(self) -> list["Attachment"]:
|
2022-06-23 19:07:20 +00:00
|
|
|
attachments = []
|
|
|
|
for obj in self.ap_object.get("attachment", []):
|
2022-06-25 06:23:28 +00:00
|
|
|
proxied_url = proxied_media_url(obj["url"])
|
2022-06-23 19:07:20 +00:00
|
|
|
attachments.append(
|
|
|
|
Attachment.parse_obj(
|
|
|
|
{
|
|
|
|
"proxiedUrl": proxied_url,
|
|
|
|
"resizedUrl": proxied_url + "/740"
|
|
|
|
if obj["mediaType"].startswith("image")
|
|
|
|
else None,
|
|
|
|
**obj,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
)
|
2022-06-22 18:11:22 +00:00
|
|
|
|
|
|
|
# Also add any video Link (for PeerTube compat)
|
|
|
|
if self.ap_type == "Video":
|
|
|
|
for link in ap.as_list(self.ap_object.get("url", [])):
|
|
|
|
if (isinstance(link, dict)) and link.get("type") == "Link":
|
|
|
|
if link.get("mediaType", "").startswith("video"):
|
2022-06-25 06:23:28 +00:00
|
|
|
proxied_url = proxied_media_url(link["href"])
|
2022-06-22 18:11:22 +00:00
|
|
|
attachments.append(
|
|
|
|
Attachment(
|
|
|
|
type="Video",
|
|
|
|
mediaType=link["mediaType"],
|
|
|
|
url=link["href"],
|
2022-06-23 19:07:20 +00:00
|
|
|
proxiedUrl=proxied_url,
|
2022-06-22 18:11:22 +00:00
|
|
|
)
|
|
|
|
)
|
|
|
|
break
|
|
|
|
|
|
|
|
return attachments
|
|
|
|
|
|
|
|
@property
|
|
|
|
def url(self) -> str | None:
|
|
|
|
obj_url = self.ap_object.get("url")
|
|
|
|
if isinstance(obj_url, str):
|
|
|
|
return obj_url
|
|
|
|
elif obj_url:
|
|
|
|
for u in ap.as_list(obj_url):
|
|
|
|
if u["mediaType"] == "text/html":
|
|
|
|
return u["href"]
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
@property
|
|
|
|
def content(self) -> str | None:
|
|
|
|
content = self.ap_object.get("content")
|
|
|
|
if not content:
|
|
|
|
return None
|
|
|
|
|
|
|
|
# PeerTube returns the content as markdown
|
|
|
|
if self.ap_object.get("mediaType") == "text/markdown":
|
|
|
|
return markdown(content, extensions=["mdx_linkify"])
|
|
|
|
|
|
|
|
return content
|
|
|
|
|
2022-06-28 19:10:22 +00:00
|
|
|
@property
|
|
|
|
def summary(self) -> str | None:
|
|
|
|
return self.ap_object.get("summary")
|
|
|
|
|
2022-06-22 18:11:22 +00:00
|
|
|
@property
|
|
|
|
def permalink_id(self) -> str:
|
|
|
|
return (
|
|
|
|
"permalink-"
|
|
|
|
+ hashlib.md5(
|
|
|
|
self.ap_id.encode(),
|
|
|
|
usedforsecurity=False,
|
|
|
|
).hexdigest()
|
|
|
|
)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def activity_object_ap_id(self) -> str | None:
|
|
|
|
if "object" in self.ap_object:
|
|
|
|
return ap.get_id(self.ap_object["object"])
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
@property
|
|
|
|
def in_reply_to(self) -> str | None:
|
|
|
|
return self.ap_object.get("inReplyTo")
|
|
|
|
|
|
|
|
|
|
|
|
def _to_camel(string: str) -> str:
|
|
|
|
cased = "".join(word.capitalize() for word in string.split("_"))
|
|
|
|
return cased[0:1].lower() + cased[1:]
|
|
|
|
|
|
|
|
|
|
|
|
class BaseModel(pydantic.BaseModel):
|
|
|
|
class Config:
|
|
|
|
alias_generator = _to_camel
|
|
|
|
|
|
|
|
|
|
|
|
class Attachment(BaseModel):
|
|
|
|
type: str
|
|
|
|
media_type: str
|
|
|
|
name: str | None
|
|
|
|
url: str
|
|
|
|
|
2022-06-23 19:07:20 +00:00
|
|
|
# Extra fields for the templates
|
|
|
|
proxied_url: str
|
|
|
|
resized_url: str | None = None
|
|
|
|
|
2022-06-22 18:11:22 +00:00
|
|
|
|
|
|
|
class RemoteObject(Object):
|
2022-06-29 22:28:07 +00:00
|
|
|
def __init__(self, raw_object: ap.RawObject, actor: Actor):
|
2022-06-22 18:11:22 +00:00
|
|
|
self._raw_object = raw_object
|
2022-06-29 22:28:07 +00:00
|
|
|
self._actor = actor
|
2022-06-22 18:11:22 +00:00
|
|
|
|
2022-06-29 22:28:07 +00:00
|
|
|
if self._actor.ap_id != ap.get_actor_id(self._raw_object):
|
|
|
|
raise ValueError(f"Invalid actor {self._actor.ap_id}")
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
async def from_raw_object(
|
|
|
|
cls,
|
|
|
|
raw_object: ap.RawObject,
|
|
|
|
actor: Actor | None = None,
|
|
|
|
):
|
2022-06-22 18:11:22 +00:00
|
|
|
# Pre-fetch the actor
|
|
|
|
actor_id = ap.get_actor_id(raw_object)
|
|
|
|
if actor_id == LOCAL_ACTOR.ap_id:
|
2022-06-29 22:28:07 +00:00
|
|
|
_actor = LOCAL_ACTOR
|
2022-06-22 18:11:22 +00:00
|
|
|
elif actor:
|
|
|
|
if actor.ap_id != actor_id:
|
|
|
|
raise ValueError(
|
|
|
|
f"Invalid actor, got {actor.ap_id}, " f"expected {actor_id}"
|
|
|
|
)
|
2022-06-29 22:28:07 +00:00
|
|
|
_actor = actor # type: ignore
|
2022-06-22 18:11:22 +00:00
|
|
|
else:
|
2022-06-29 22:28:07 +00:00
|
|
|
_actor = RemoteActor(
|
|
|
|
ap_actor=await ap.fetch(ap.get_actor_id(raw_object)),
|
2022-06-22 18:11:22 +00:00
|
|
|
)
|
|
|
|
|
2022-06-29 22:28:07 +00:00
|
|
|
return cls(raw_object, _actor)
|
2022-06-22 18:11:22 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def og_meta(self) -> list[dict[str, Any]] | None:
|
|
|
|
return None
|
|
|
|
|
|
|
|
@property
|
|
|
|
def ap_object(self) -> ap.RawObject:
|
|
|
|
return self._raw_object
|
|
|
|
|
|
|
|
@property
|
|
|
|
def actor(self) -> Actor:
|
|
|
|
return self._actor
|