active-boxes 0.0.1.dev2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- active_boxes/__init__.py +12 -0
- active_boxes/__version__.py +11 -0
- active_boxes/activitypub.py +984 -0
- active_boxes/backend.py +128 -0
- active_boxes/collection.py +70 -0
- active_boxes/content_helper.py +69 -0
- active_boxes/errors.py +92 -0
- active_boxes/httpsig.py +145 -0
- active_boxes/key.py +63 -0
- active_boxes/linked_data_sig.py +83 -0
- active_boxes/urlutils.py +66 -0
- active_boxes/webfinger.py +92 -0
- active_boxes-0.0.1.dev2.dist-info/LICENSE +22 -0
- active_boxes-0.0.1.dev2.dist-info/METADATA +45 -0
- active_boxes-0.0.1.dev2.dist-info/RECORD +16 -0
- active_boxes-0.0.1.dev2.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,984 @@
|
|
|
1
|
+
"""Core ActivityPub classes."""
|
|
2
|
+
import logging
|
|
3
|
+
import weakref
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from datetime import timezone
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any
|
|
8
|
+
from typing import Dict
|
|
9
|
+
from typing import List
|
|
10
|
+
from typing import Optional
|
|
11
|
+
from typing import Type
|
|
12
|
+
from typing import Union
|
|
13
|
+
|
|
14
|
+
from .backend import Backend
|
|
15
|
+
from .errors import ActivityGoneError
|
|
16
|
+
from .errors import ActivityNotFoundError
|
|
17
|
+
from .errors import ActivityUnavailableError
|
|
18
|
+
from .errors import BadActivityError
|
|
19
|
+
from .errors import NotAnActivityError
|
|
20
|
+
from .errors import Error
|
|
21
|
+
from .errors import UnexpectedActivityTypeError
|
|
22
|
+
from .key import Key
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
UninitializedBackendError = Error("a backend must be initialized")
|
|
27
|
+
|
|
28
|
+
# Helper/shortcut for typing
|
|
29
|
+
ObjectType = Dict[str, Any]
|
|
30
|
+
ActorType = Union["Person", "Application", "Group", "Organization", "Service"]
|
|
31
|
+
ObjectOrIDType = Union[str, ObjectType]
|
|
32
|
+
|
|
33
|
+
CTX_AS = "https://www.w3.org/ns/activitystreams"
|
|
34
|
+
CTX_SECURITY = "https://w3id.org/security/v1"
|
|
35
|
+
AS_PUBLIC = "https://www.w3.org/ns/activitystreams#Public"
|
|
36
|
+
|
|
37
|
+
DEFAULT_CTX = COLLECTION_CTX = [
|
|
38
|
+
"https://www.w3.org/ns/activitystreams",
|
|
39
|
+
"https://w3id.org/security/v1",
|
|
40
|
+
{
|
|
41
|
+
# AS ext
|
|
42
|
+
"Hashtag": "as:Hashtag",
|
|
43
|
+
"sensitive": "as:sensitive",
|
|
44
|
+
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
|
45
|
+
# toot
|
|
46
|
+
"toot": "http://joinmastodon.org/ns#",
|
|
47
|
+
"featured": "toot:featured",
|
|
48
|
+
# schema
|
|
49
|
+
"schema": "http://schema.org#",
|
|
50
|
+
"PropertyValue": "schema:PropertyValue",
|
|
51
|
+
"value": "schema:value",
|
|
52
|
+
},
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
# Will be used to keep track of all the defined activities
|
|
56
|
+
_ACTIVITY_CLS: Dict["ActivityType", Type["BaseActivity"]] = {}
|
|
57
|
+
|
|
58
|
+
BACKEND: Optional[Backend] = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_backend() -> Backend:
|
|
62
|
+
if BACKEND is None:
|
|
63
|
+
raise UninitializedBackendError
|
|
64
|
+
return BACKEND
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def use_backend(backend_instance):
|
|
68
|
+
global BACKEND
|
|
69
|
+
BACKEND = backend_instance
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def format_datetime(dt: datetime) -> str:
|
|
73
|
+
if dt.tzinfo is None:
|
|
74
|
+
raise ValueError("datetime must be tz aware")
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
dt.astimezone(timezone.utc)
|
|
78
|
+
.replace(microsecond=0)
|
|
79
|
+
.isoformat()
|
|
80
|
+
.replace("+00:00", "Z")
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ActivityType(Enum):
|
|
85
|
+
"""Supported activity `type`."""
|
|
86
|
+
|
|
87
|
+
ANNOUNCE = "Announce"
|
|
88
|
+
BLOCK = "Block"
|
|
89
|
+
LIKE = "Like"
|
|
90
|
+
CREATE = "Create"
|
|
91
|
+
UPDATE = "Update"
|
|
92
|
+
|
|
93
|
+
ORDERED_COLLECTION = "OrderedCollection"
|
|
94
|
+
ORDERED_COLLECTION_PAGE = "OrderedCollectionPage"
|
|
95
|
+
COLLECTION_PAGE = "CollectionPage"
|
|
96
|
+
COLLECTION = "Collection"
|
|
97
|
+
|
|
98
|
+
NOTE = "Note"
|
|
99
|
+
ARTICLE = "Article"
|
|
100
|
+
VIDEO = "Video"
|
|
101
|
+
AUDIO = "Audio"
|
|
102
|
+
DOCUMENT = "Document"
|
|
103
|
+
|
|
104
|
+
ACCEPT = "Accept"
|
|
105
|
+
REJECT = "Reject"
|
|
106
|
+
FOLLOW = "Follow"
|
|
107
|
+
|
|
108
|
+
DELETE = "Delete"
|
|
109
|
+
UNDO = "Undo"
|
|
110
|
+
|
|
111
|
+
IMAGE = "Image"
|
|
112
|
+
TOMBSTONE = "Tombstone"
|
|
113
|
+
|
|
114
|
+
# Actor types
|
|
115
|
+
PERSON = "Person"
|
|
116
|
+
APPLICATION = "Application"
|
|
117
|
+
GROUP = "Group"
|
|
118
|
+
ORGANIZATION = "Organization"
|
|
119
|
+
SERVICE = "Service"
|
|
120
|
+
|
|
121
|
+
# Others
|
|
122
|
+
MENTION = "Mention"
|
|
123
|
+
|
|
124
|
+
# Mastodon specific?
|
|
125
|
+
QUESTION = "Question"
|
|
126
|
+
|
|
127
|
+
# Used by Prismo
|
|
128
|
+
PAGE = "Page"
|
|
129
|
+
|
|
130
|
+
# Misskey uses standalone Key object
|
|
131
|
+
KEY = "Key"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
ACTOR_TYPES = [
|
|
135
|
+
ActivityType.PERSON,
|
|
136
|
+
ActivityType.APPLICATION,
|
|
137
|
+
ActivityType.GROUP,
|
|
138
|
+
ActivityType.ORGANIZATION,
|
|
139
|
+
ActivityType.SERVICE,
|
|
140
|
+
ActivityType.QUESTION, # Mastodon notoft the end of a question with an update from that question
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
CREATE_TYPES = [
|
|
144
|
+
ActivityType.NOTE,
|
|
145
|
+
ActivityType.ARTICLE,
|
|
146
|
+
ActivityType.VIDEO,
|
|
147
|
+
ActivityType.AUDIO,
|
|
148
|
+
ActivityType.QUESTION,
|
|
149
|
+
ActivityType.DOCUMENT,
|
|
150
|
+
ActivityType.PAGE,
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
COLLECTION_TYPES = [ActivityType.COLLECTION, ActivityType.ORDERED_COLLECTION]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def parse_activity(
|
|
157
|
+
payload: ObjectType, expected: Optional[ActivityType] = None
|
|
158
|
+
) -> "BaseActivity":
|
|
159
|
+
if "type" not in payload:
|
|
160
|
+
raise BadActivityError(f"the payload has no type: {payload!r}")
|
|
161
|
+
|
|
162
|
+
t = ActivityType(_to_list(payload["type"])[0])
|
|
163
|
+
|
|
164
|
+
if expected and t != expected:
|
|
165
|
+
raise UnexpectedActivityTypeError(
|
|
166
|
+
f'expected a {expected.name} activity, got a {payload["type"]}: {payload}'
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if t not in _ACTIVITY_CLS:
|
|
170
|
+
raise BadActivityError(
|
|
171
|
+
f'unsupported activity type {payload["type"]}: {payload}'
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
activity = _ACTIVITY_CLS[t](**payload)
|
|
175
|
+
|
|
176
|
+
return activity
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _to_list(data: Union[List[Any], Any]) -> List[Any]:
|
|
180
|
+
"""Helper to convert fields that can be either an object or a list of objects to a
|
|
181
|
+
list of object."""
|
|
182
|
+
if isinstance(data, list):
|
|
183
|
+
return data
|
|
184
|
+
return [data]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def clean_activity(activity: ObjectType) -> Dict[str, Any]:
|
|
188
|
+
"""Clean the activity before rendering it.
|
|
189
|
+
- Remove the hidden bco and bcc field
|
|
190
|
+
"""
|
|
191
|
+
for field in ["bto", "bcc", "source"]:
|
|
192
|
+
if field in activity:
|
|
193
|
+
del activity[field]
|
|
194
|
+
if activity["type"] == "Create" and field in activity["object"]:
|
|
195
|
+
del activity["object"][field]
|
|
196
|
+
return activity
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _get_actor_id(actor: ObjectOrIDType) -> str:
|
|
200
|
+
"""Helper for retrieving an actor `id`."""
|
|
201
|
+
if isinstance(actor, dict):
|
|
202
|
+
return actor["id"]
|
|
203
|
+
return actor
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _get_id(obj) -> Optional[str]:
|
|
207
|
+
if obj is None:
|
|
208
|
+
return None
|
|
209
|
+
elif isinstance(obj, str):
|
|
210
|
+
return obj
|
|
211
|
+
elif isinstance(obj, dict):
|
|
212
|
+
try:
|
|
213
|
+
return obj["id"]
|
|
214
|
+
except KeyError:
|
|
215
|
+
raise ValueError(f"object is missing ID: {obj!r}")
|
|
216
|
+
else:
|
|
217
|
+
raise ValueError(f"unexpected object: {obj!r}")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _has_type(
|
|
221
|
+
obj_type: Union[str, List[str]],
|
|
222
|
+
_types: Union[ActivityType, str, List[Union[ActivityType, str]]],
|
|
223
|
+
):
|
|
224
|
+
"""Returns `True` if one of `obj_type` equals one of `_types`."""
|
|
225
|
+
types_str = [
|
|
226
|
+
_type.value if isinstance(_type, ActivityType) else _type
|
|
227
|
+
for _type in _to_list(_types)
|
|
228
|
+
]
|
|
229
|
+
for _type in _to_list(obj_type):
|
|
230
|
+
if _type in types_str:
|
|
231
|
+
return True
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class _ActivityMeta(type):
|
|
236
|
+
"""Metaclass for keeping track of subclass."""
|
|
237
|
+
|
|
238
|
+
def __new__(meta, name, bases, class_dict):
|
|
239
|
+
cls = type.__new__(meta, name, bases, class_dict)
|
|
240
|
+
|
|
241
|
+
# Ensure the class has an activity type defined
|
|
242
|
+
if name != "BaseActivity" and not cls.ACTIVITY_TYPE:
|
|
243
|
+
raise ValueError(f"class {name} has no ACTIVITY_TYPE")
|
|
244
|
+
|
|
245
|
+
# Register it
|
|
246
|
+
_ACTIVITY_CLS[cls.ACTIVITY_TYPE] = cls
|
|
247
|
+
return cls
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class BaseActivity(object, metaclass=_ActivityMeta):
|
|
251
|
+
"""Base class for ActivityPub activities."""
|
|
252
|
+
|
|
253
|
+
ACTIVITY_TYPE: Optional[
|
|
254
|
+
ActivityType
|
|
255
|
+
] = None # the ActivityTypeEnum the class will represent
|
|
256
|
+
OBJECT_REQUIRED = False # Whether the object field is required or note
|
|
257
|
+
ALLOWED_OBJECT_TYPES: List[ActivityType] = []
|
|
258
|
+
ACTOR_REQUIRED = (
|
|
259
|
+
True
|
|
260
|
+
) # Most of the object requires an actor, so this flag in on by default
|
|
261
|
+
|
|
262
|
+
def __init__(self, **kwargs) -> None: # noqa: C901
|
|
263
|
+
if not self.ACTIVITY_TYPE:
|
|
264
|
+
raise Error("should never happen")
|
|
265
|
+
|
|
266
|
+
# Initialize the dict that will contains all the activity fields
|
|
267
|
+
self._data: Dict[str, Any] = {}
|
|
268
|
+
|
|
269
|
+
if not kwargs.get("type"):
|
|
270
|
+
self._data["type"] = self.ACTIVITY_TYPE.value
|
|
271
|
+
else:
|
|
272
|
+
atype = kwargs.pop("type")
|
|
273
|
+
if self.ACTIVITY_TYPE.value not in _to_list(atype):
|
|
274
|
+
raise UnexpectedActivityTypeError(
|
|
275
|
+
f"Expect the type to be {self.ACTIVITY_TYPE.value!r}"
|
|
276
|
+
)
|
|
277
|
+
self._data["type"] = atype
|
|
278
|
+
|
|
279
|
+
logger.debug(f"initializing a {self.ACTIVITY_TYPE.value} activity: {kwargs!r}")
|
|
280
|
+
|
|
281
|
+
# A place to set ephemeral data
|
|
282
|
+
self.__ctx: Any = {}
|
|
283
|
+
|
|
284
|
+
self.__obj: Optional["BaseActivity"] = None
|
|
285
|
+
self.__actor: Optional[List[ActorType]] = None
|
|
286
|
+
|
|
287
|
+
# The id may not be present for new activities
|
|
288
|
+
if "id" in kwargs:
|
|
289
|
+
self._data["id"] = kwargs.pop("id")
|
|
290
|
+
|
|
291
|
+
if self.ACTIVITY_TYPE not in ACTOR_TYPES and self.ACTOR_REQUIRED:
|
|
292
|
+
actor = kwargs.get("actor")
|
|
293
|
+
if actor:
|
|
294
|
+
kwargs.pop("actor")
|
|
295
|
+
actor = self._validate_actor(actor)
|
|
296
|
+
self._data["actor"] = actor
|
|
297
|
+
elif self.ACTIVITY_TYPE in CREATE_TYPES:
|
|
298
|
+
if "attributedTo" not in kwargs:
|
|
299
|
+
raise BadActivityError(f"Note is missing attributedTo")
|
|
300
|
+
else:
|
|
301
|
+
raise BadActivityError("missing actor")
|
|
302
|
+
|
|
303
|
+
if self.OBJECT_REQUIRED and "object" in kwargs:
|
|
304
|
+
obj = kwargs.pop("object")
|
|
305
|
+
if isinstance(obj, str):
|
|
306
|
+
# The object is a just a reference the its ID/IRI
|
|
307
|
+
# FIXME(tsileo): fetch the ref
|
|
308
|
+
self._data["object"] = obj
|
|
309
|
+
elif isinstance(obj, dict):
|
|
310
|
+
if not self.ALLOWED_OBJECT_TYPES:
|
|
311
|
+
raise UnexpectedActivityTypeError("unexpected object")
|
|
312
|
+
if "type" not in obj or (
|
|
313
|
+
self.ACTIVITY_TYPE != ActivityType.CREATE and "id" not in obj
|
|
314
|
+
):
|
|
315
|
+
raise BadActivityError("invalid object, missing type")
|
|
316
|
+
if not _has_type( # type: ignore # XXX too complicated
|
|
317
|
+
obj["type"], self.ALLOWED_OBJECT_TYPES
|
|
318
|
+
):
|
|
319
|
+
raise UnexpectedActivityTypeError(
|
|
320
|
+
f'unexpected object type {obj["type"]} (allowed={self.ALLOWED_OBJECT_TYPES!r})'
|
|
321
|
+
)
|
|
322
|
+
self._data["object"] = obj
|
|
323
|
+
else:
|
|
324
|
+
raise BadActivityError(
|
|
325
|
+
f"invalid object type ({type(obj).__qualname__}): {obj!r}"
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
if "@context" not in kwargs:
|
|
329
|
+
self._data["@context"] = CTX_AS
|
|
330
|
+
else:
|
|
331
|
+
self._data["@context"] = kwargs.pop("@context")
|
|
332
|
+
|
|
333
|
+
# @context check
|
|
334
|
+
if not isinstance(self._data["@context"], list):
|
|
335
|
+
self._data["@context"] = [self._data["@context"]]
|
|
336
|
+
if CTX_SECURITY not in self._data["@context"]:
|
|
337
|
+
self._data["@context"].append(CTX_SECURITY)
|
|
338
|
+
if isinstance(self._data["@context"][-1], dict):
|
|
339
|
+
self._data["@context"][-1]["Hashtag"] = "as:Hashtag"
|
|
340
|
+
self._data["@context"][-1]["sensitive"] = "as:sensitive"
|
|
341
|
+
self._data["@context"][-1]["toot"] = "http://joinmastodon.org/ns#"
|
|
342
|
+
self._data["@context"][-1]["featured"] = "toot:featured"
|
|
343
|
+
else:
|
|
344
|
+
self._data["@context"].append(
|
|
345
|
+
{
|
|
346
|
+
"Hashtag": "as:Hashtag",
|
|
347
|
+
"sensitive": "as:sensitive",
|
|
348
|
+
"toot": "http://joinmastodon.org/ns#",
|
|
349
|
+
"featured": "toot:featured",
|
|
350
|
+
}
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# Remove keys with `None` value
|
|
354
|
+
valid_kwargs = {}
|
|
355
|
+
for k, v in kwargs.items():
|
|
356
|
+
if v is None:
|
|
357
|
+
continue
|
|
358
|
+
valid_kwargs[k] = v
|
|
359
|
+
self._data.update(**valid_kwargs)
|
|
360
|
+
|
|
361
|
+
try:
|
|
362
|
+
self._init()
|
|
363
|
+
except NotImplementedError:
|
|
364
|
+
pass
|
|
365
|
+
|
|
366
|
+
def _init(self) -> None:
|
|
367
|
+
"""Optional init callback."""
|
|
368
|
+
raise NotImplementedError
|
|
369
|
+
|
|
370
|
+
def has_type(
|
|
371
|
+
self, _types: Union[ActivityType, str, List[Union[ActivityType, str]]]
|
|
372
|
+
):
|
|
373
|
+
"""Return True if the activity has the given type."""
|
|
374
|
+
return _has_type(self._data["type"], _types)
|
|
375
|
+
|
|
376
|
+
def get_url(self, preferred_mimetype: str = "text/html") -> str:
|
|
377
|
+
"""Returns the url attributes as a str.
|
|
378
|
+
|
|
379
|
+
Returns the URL if it's a str, or the href of the first link.
|
|
380
|
+
|
|
381
|
+
"""
|
|
382
|
+
if isinstance(self.url, str):
|
|
383
|
+
return self.url
|
|
384
|
+
elif isinstance(self.url, dict):
|
|
385
|
+
if self.url.get("type") != "Link":
|
|
386
|
+
raise BadActivityError(f"invalid type {self.url}")
|
|
387
|
+
return str(self.url.get("href"))
|
|
388
|
+
elif isinstance(self.url, list):
|
|
389
|
+
last_link = None
|
|
390
|
+
for link in self.url:
|
|
391
|
+
last_link = link
|
|
392
|
+
if link.get("type") != "Link":
|
|
393
|
+
raise BadActivityError(f"invalid type {link}")
|
|
394
|
+
if link.get("mimeType").startswith(preferred_mimetype):
|
|
395
|
+
return link.get("href")
|
|
396
|
+
if not last_link:
|
|
397
|
+
raise BadActivityError(f"invalid type for {self.url}")
|
|
398
|
+
return last_link
|
|
399
|
+
else:
|
|
400
|
+
raise BadActivityError(f"invalid type for {self.url}")
|
|
401
|
+
|
|
402
|
+
def ctx(self) -> Any:
|
|
403
|
+
if self.__ctx:
|
|
404
|
+
return self.__ctx()
|
|
405
|
+
|
|
406
|
+
def set_ctx(self, ctx: Any) -> None:
|
|
407
|
+
# FIXME(tsileo): does not use the ctx to set the id to the "parent" when building delete
|
|
408
|
+
self.__ctx = weakref.ref(ctx)
|
|
409
|
+
|
|
410
|
+
def __repr__(self) -> str:
|
|
411
|
+
"""Pretty repr."""
|
|
412
|
+
return "{}({!r})".format(self.__class__.__qualname__, self._data.get("id"))
|
|
413
|
+
|
|
414
|
+
def __str__(self) -> str:
|
|
415
|
+
"""Returns the ID/IRI when castign to str."""
|
|
416
|
+
return str(self._data.get("id", f"[new {self.ACTIVITY_TYPE} activity]"))
|
|
417
|
+
|
|
418
|
+
def __getattr__(self, name: str) -> Any:
|
|
419
|
+
"""Allow to access the object field as regular attributes."""
|
|
420
|
+
if self._data.get(name):
|
|
421
|
+
return self._data.get(name)
|
|
422
|
+
|
|
423
|
+
def _set_id(self, uri: str, obj_id: str) -> None:
|
|
424
|
+
"""Optional callback for subclasses to so something with a newly generated ID (for outbox activities)."""
|
|
425
|
+
raise NotImplementedError
|
|
426
|
+
|
|
427
|
+
def set_id(self, uri: str, obj_id: str) -> None:
|
|
428
|
+
"""Set the ID for a new activity."""
|
|
429
|
+
logger.debug(f"setting ID {uri} / {obj_id}")
|
|
430
|
+
self._data["id"] = uri
|
|
431
|
+
try:
|
|
432
|
+
self._set_id(uri, obj_id)
|
|
433
|
+
except NotImplementedError:
|
|
434
|
+
pass
|
|
435
|
+
|
|
436
|
+
def _actor_id(self, obj: ObjectOrIDType) -> str:
|
|
437
|
+
if isinstance(obj, dict) and _has_type( # type: ignore
|
|
438
|
+
obj["type"], ACTOR_TYPES
|
|
439
|
+
):
|
|
440
|
+
obj_id = obj.get("id")
|
|
441
|
+
if not obj_id:
|
|
442
|
+
raise BadActivityError(f"missing object id: {obj!r}")
|
|
443
|
+
return obj_id
|
|
444
|
+
elif isinstance(obj, str):
|
|
445
|
+
return obj
|
|
446
|
+
else:
|
|
447
|
+
raise BadActivityError(f'invalid "actor" field: {obj!r}')
|
|
448
|
+
|
|
449
|
+
def _validate_actor(self, obj: ObjectOrIDType) -> str:
|
|
450
|
+
if BACKEND is None:
|
|
451
|
+
raise UninitializedBackendError
|
|
452
|
+
|
|
453
|
+
obj_id = self._actor_id(obj)
|
|
454
|
+
try:
|
|
455
|
+
actor = BACKEND.fetch_iri(obj_id)
|
|
456
|
+
except (ActivityGoneError, ActivityNotFoundError):
|
|
457
|
+
raise
|
|
458
|
+
except Exception:
|
|
459
|
+
raise BadActivityError(f"failed to validate actor {obj!r}")
|
|
460
|
+
|
|
461
|
+
if not actor or "id" not in actor:
|
|
462
|
+
raise BadActivityError(f"invalid actor {actor}")
|
|
463
|
+
|
|
464
|
+
if not _has_type( # type: ignore # XXX: too complicated
|
|
465
|
+
actor["type"], ACTOR_TYPES
|
|
466
|
+
):
|
|
467
|
+
raise UnexpectedActivityTypeError(f'actor has wrong type {actor["type"]!r}')
|
|
468
|
+
|
|
469
|
+
return actor["id"]
|
|
470
|
+
|
|
471
|
+
def get_object_id(self) -> str:
|
|
472
|
+
if BACKEND is None:
|
|
473
|
+
raise UninitializedBackendError
|
|
474
|
+
|
|
475
|
+
if self.__obj:
|
|
476
|
+
return self.__obj.id
|
|
477
|
+
if isinstance(self._data["object"], dict):
|
|
478
|
+
return self._data["object"]["id"]
|
|
479
|
+
elif isinstance(self._data["object"], str):
|
|
480
|
+
return self._data["object"]
|
|
481
|
+
else:
|
|
482
|
+
raise ValueError(f"invalid object {self._data['object']}")
|
|
483
|
+
|
|
484
|
+
def get_object(self) -> "BaseActivity":
|
|
485
|
+
"""Returns the object as a BaseActivity instance."""
|
|
486
|
+
if BACKEND is None:
|
|
487
|
+
raise UninitializedBackendError
|
|
488
|
+
|
|
489
|
+
if self.__obj:
|
|
490
|
+
return self.__obj
|
|
491
|
+
if isinstance(self._data["object"], dict):
|
|
492
|
+
p = parse_activity(self._data["object"])
|
|
493
|
+
else:
|
|
494
|
+
obj = BACKEND.fetch_iri(self._data["object"])
|
|
495
|
+
if ActivityType(obj.get("type")) not in self.ALLOWED_OBJECT_TYPES:
|
|
496
|
+
raise UnexpectedActivityTypeError(
|
|
497
|
+
f'invalid object type {obj.get("type")!r}'
|
|
498
|
+
)
|
|
499
|
+
p = parse_activity(obj)
|
|
500
|
+
|
|
501
|
+
self.__obj = p
|
|
502
|
+
return p
|
|
503
|
+
|
|
504
|
+
def reset_object_cache(self) -> None:
|
|
505
|
+
self.__obj = None
|
|
506
|
+
|
|
507
|
+
def to_dict(
|
|
508
|
+
self, embed: bool = False, embed_object_id_only: bool = False
|
|
509
|
+
) -> ObjectType:
|
|
510
|
+
"""Serializes the activity back to a dict, ready to be JSON serialized."""
|
|
511
|
+
data = dict(self._data)
|
|
512
|
+
if embed:
|
|
513
|
+
for k in ["@context", "signature"]:
|
|
514
|
+
if k in data:
|
|
515
|
+
del data[k]
|
|
516
|
+
if (
|
|
517
|
+
data.get("object")
|
|
518
|
+
and embed_object_id_only
|
|
519
|
+
and isinstance(data["object"], dict)
|
|
520
|
+
):
|
|
521
|
+
try:
|
|
522
|
+
data["object"] = data["object"]["id"]
|
|
523
|
+
except KeyError:
|
|
524
|
+
raise BadActivityError(
|
|
525
|
+
f'embedded object {data["object"]!r} should have an id'
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
return data
|
|
529
|
+
|
|
530
|
+
def get_actor(self) -> ActorType:
|
|
531
|
+
if BACKEND is None:
|
|
532
|
+
raise UninitializedBackendError
|
|
533
|
+
|
|
534
|
+
if self.__actor:
|
|
535
|
+
return self.__actor[0]
|
|
536
|
+
|
|
537
|
+
actor = self._data.get("actor")
|
|
538
|
+
if not actor and self.ACTOR_REQUIRED:
|
|
539
|
+
# Quick hack for Note objects
|
|
540
|
+
if self.ACTIVITY_TYPE in CREATE_TYPES:
|
|
541
|
+
actor = self._data.get("attributedTo")
|
|
542
|
+
if not actor:
|
|
543
|
+
raise BadActivityError(f"missing attributedTo")
|
|
544
|
+
else:
|
|
545
|
+
raise BadActivityError(f"failed to fetch actor: {self._data!r}")
|
|
546
|
+
|
|
547
|
+
self.__actor: List[ActorType] = []
|
|
548
|
+
for item in _to_list(actor):
|
|
549
|
+
if not isinstance(item, (str, dict)):
|
|
550
|
+
raise BadActivityError(f"invalid actor: {self._data!r}")
|
|
551
|
+
|
|
552
|
+
actor_id = self._actor_id(item)
|
|
553
|
+
|
|
554
|
+
p = parse_activity(BACKEND.fetch_iri(actor_id))
|
|
555
|
+
if not p.has_type(ACTOR_TYPES): # type: ignore
|
|
556
|
+
raise UnexpectedActivityTypeError(f"{p!r} is not an actor")
|
|
557
|
+
self.__actor.append(p) # type: ignore
|
|
558
|
+
|
|
559
|
+
return self.__actor[0]
|
|
560
|
+
|
|
561
|
+
def _recipients(self) -> List[str]:
|
|
562
|
+
return []
|
|
563
|
+
|
|
564
|
+
def recipients(self) -> List[str]: # noqa: C901
|
|
565
|
+
if BACKEND is None:
|
|
566
|
+
raise UninitializedBackendError
|
|
567
|
+
|
|
568
|
+
recipients = self._recipients()
|
|
569
|
+
actor_id = self.get_actor().id
|
|
570
|
+
|
|
571
|
+
out: List[str] = []
|
|
572
|
+
if self.type == ActivityType.CREATE.value:
|
|
573
|
+
out = BACKEND.extra_inboxes()
|
|
574
|
+
|
|
575
|
+
for recipient in recipients:
|
|
576
|
+
if recipient in [actor_id, AS_PUBLIC, None]:
|
|
577
|
+
continue
|
|
578
|
+
|
|
579
|
+
try:
|
|
580
|
+
actor = fetch_remote_activity(recipient)
|
|
581
|
+
except (ActivityGoneError, ActivityNotFoundError, NotAnActivityError):
|
|
582
|
+
logger.info(f"{recipient} is gone")
|
|
583
|
+
continue
|
|
584
|
+
except ActivityUnavailableError:
|
|
585
|
+
# TODO(tsileo): retry separately?
|
|
586
|
+
logger.info(f"failed {recipient} to fetch recipient")
|
|
587
|
+
continue
|
|
588
|
+
|
|
589
|
+
if actor.ACTIVITY_TYPE in ACTOR_TYPES:
|
|
590
|
+
if actor.endpoints:
|
|
591
|
+
shared_inbox = actor.endpoints.get("sharedInbox")
|
|
592
|
+
if shared_inbox:
|
|
593
|
+
if shared_inbox not in out:
|
|
594
|
+
out.append(shared_inbox)
|
|
595
|
+
continue
|
|
596
|
+
|
|
597
|
+
if actor.inbox and actor.inbox not in out:
|
|
598
|
+
out.append(actor.inbox)
|
|
599
|
+
|
|
600
|
+
# Is the activity a `Collection`/`OrderedCollection`?
|
|
601
|
+
elif actor.ACTIVITY_TYPE in COLLECTION_TYPES:
|
|
602
|
+
for item in BACKEND.parse_collection(actor.to_dict()):
|
|
603
|
+
# XXX(tsileo): is nested collection support needed here?
|
|
604
|
+
|
|
605
|
+
if item in [actor_id, AS_PUBLIC]:
|
|
606
|
+
continue
|
|
607
|
+
|
|
608
|
+
try:
|
|
609
|
+
col_actor = fetch_remote_activity(item)
|
|
610
|
+
except ActivityUnavailableError:
|
|
611
|
+
# TODO(tsileo): retry separately?
|
|
612
|
+
logger.info(f"failed {recipient} to fetch recipient")
|
|
613
|
+
continue
|
|
614
|
+
except (
|
|
615
|
+
ActivityGoneError,
|
|
616
|
+
ActivityNotFoundError,
|
|
617
|
+
NotAnActivityError,
|
|
618
|
+
):
|
|
619
|
+
logger.info(f"{item} is gone")
|
|
620
|
+
continue
|
|
621
|
+
|
|
622
|
+
if col_actor.endpoints:
|
|
623
|
+
shared_inbox = col_actor.endpoints.get("sharedInbox")
|
|
624
|
+
if shared_inbox:
|
|
625
|
+
if shared_inbox not in out:
|
|
626
|
+
out.append(shared_inbox)
|
|
627
|
+
continue
|
|
628
|
+
|
|
629
|
+
if col_actor.inbox and col_actor.inbox not in out:
|
|
630
|
+
out.append(col_actor.inbox)
|
|
631
|
+
else:
|
|
632
|
+
raise BadActivityError(f"failed to parse {recipient}")
|
|
633
|
+
|
|
634
|
+
return out
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
class Person(BaseActivity):
|
|
638
|
+
ACTIVITY_TYPE = ActivityType.PERSON
|
|
639
|
+
OBJECT_REQUIRED = False
|
|
640
|
+
ACTOR_REQUIRED = False
|
|
641
|
+
|
|
642
|
+
def get_key(self) -> Key:
|
|
643
|
+
return Key.from_dict(self.publicKey)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
class Service(Person):
|
|
647
|
+
ACTIVITY_TYPE = ActivityType.SERVICE
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
class Application(Person):
|
|
651
|
+
ACTIVITY_TYPE = ActivityType.APPLICATION
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
class Group(Person):
|
|
655
|
+
ACTIVITY_TYPE = ActivityType.GROUP
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
class Organization(Person):
|
|
659
|
+
ACTIVITY_TYPE = ActivityType.ORGANIZATION
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
class Block(BaseActivity):
|
|
663
|
+
ACTIVITY_TYPE = ActivityType.BLOCK
|
|
664
|
+
OBJECT_REQUIRED = True
|
|
665
|
+
ACTOR_REQUIRED = True
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
class Collection(BaseActivity):
|
|
669
|
+
ACTIVITY_TYPE = ActivityType.COLLECTION
|
|
670
|
+
OBJECT_REQUIRED = False
|
|
671
|
+
ACTOR_REQUIRED = False
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
class OerderedCollection(BaseActivity):
|
|
675
|
+
ACTIVITY_TYPE = ActivityType.ORDERED_COLLECTION
|
|
676
|
+
OBJECT_REQUIRED = False
|
|
677
|
+
ACTOR_REQUIRED = False
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
class Image(BaseActivity):
|
|
681
|
+
ACTIVITY_TYPE = ActivityType.IMAGE
|
|
682
|
+
OBJECT_REQUIRED = False
|
|
683
|
+
ACTOR_REQUIRED = False
|
|
684
|
+
|
|
685
|
+
def __repr__(self):
|
|
686
|
+
return "Image({!r})".format(self._data.get("url"))
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
class Follow(BaseActivity):
|
|
690
|
+
ACTIVITY_TYPE = ActivityType.FOLLOW
|
|
691
|
+
ALLOWED_OBJECT_TYPES = ACTOR_TYPES
|
|
692
|
+
OBJECT_REQUIRED = True
|
|
693
|
+
ACTOR_REQUIRED = True
|
|
694
|
+
|
|
695
|
+
def _recipients(self) -> List[str]:
|
|
696
|
+
return [self.get_object().id]
|
|
697
|
+
|
|
698
|
+
def build_undo(self) -> BaseActivity:
|
|
699
|
+
return Undo(object=self.to_dict(embed=True), actor=self.get_actor().id)
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
class Accept(BaseActivity):
|
|
703
|
+
ACTIVITY_TYPE = ActivityType.ACCEPT
|
|
704
|
+
ALLOWED_OBJECT_TYPES = [ActivityType.FOLLOW]
|
|
705
|
+
OBJECT_REQUIRED = True
|
|
706
|
+
ACTOR_REQUIRED = True
|
|
707
|
+
|
|
708
|
+
def _recipients(self) -> List[str]:
|
|
709
|
+
return [self.get_object().get_actor().id]
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
class Undo(BaseActivity):
|
|
713
|
+
ACTIVITY_TYPE = ActivityType.UNDO
|
|
714
|
+
ALLOWED_OBJECT_TYPES = [
|
|
715
|
+
ActivityType.FOLLOW,
|
|
716
|
+
ActivityType.LIKE,
|
|
717
|
+
ActivityType.ANNOUNCE,
|
|
718
|
+
ActivityType.BLOCK,
|
|
719
|
+
]
|
|
720
|
+
OBJECT_REQUIRED = True
|
|
721
|
+
ACTOR_REQUIRED = True
|
|
722
|
+
|
|
723
|
+
def _recipients(self) -> List[str]:
|
|
724
|
+
obj = self.get_object()
|
|
725
|
+
if obj.ACTIVITY_TYPE == ActivityType.FOLLOW:
|
|
726
|
+
return [obj.get_object().id]
|
|
727
|
+
else:
|
|
728
|
+
return [obj.get_object().get_actor().id]
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
class Like(BaseActivity):
|
|
732
|
+
ACTIVITY_TYPE = ActivityType.LIKE
|
|
733
|
+
ALLOWED_OBJECT_TYPES = CREATE_TYPES
|
|
734
|
+
OBJECT_REQUIRED = True
|
|
735
|
+
ACTOR_REQUIRED = True
|
|
736
|
+
|
|
737
|
+
def _recipients(self) -> List[str]:
|
|
738
|
+
return [self.get_object().get_actor().id]
|
|
739
|
+
|
|
740
|
+
def build_undo(self) -> BaseActivity:
|
|
741
|
+
return Undo(
|
|
742
|
+
object=self.to_dict(embed=True, embed_object_id_only=True),
|
|
743
|
+
actor=self.get_actor().id,
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
class Announce(BaseActivity):
|
|
748
|
+
ACTIVITY_TYPE = ActivityType.ANNOUNCE
|
|
749
|
+
ALLOWED_OBJECT_TYPES = CREATE_TYPES
|
|
750
|
+
OBJECT_REQUIRED = True
|
|
751
|
+
ACTOR_REQUIRED = True
|
|
752
|
+
|
|
753
|
+
def _recipients(self) -> List[str]:
|
|
754
|
+
recipients = [self.get_object().get_actor().id]
|
|
755
|
+
|
|
756
|
+
for field in ["to", "cc"]:
|
|
757
|
+
if field in self._data:
|
|
758
|
+
recipients.extend(_to_list(self._data[field]))
|
|
759
|
+
|
|
760
|
+
return list(set(recipients))
|
|
761
|
+
|
|
762
|
+
def build_undo(self) -> BaseActivity:
|
|
763
|
+
return Undo(actor=self.get_actor().id, object=self.to_dict(embed=True))
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
class Delete(BaseActivity):
|
|
767
|
+
ACTIVITY_TYPE = ActivityType.DELETE
|
|
768
|
+
ALLOWED_OBJECT_TYPES = CREATE_TYPES + ACTOR_TYPES + [ActivityType.TOMBSTONE]
|
|
769
|
+
OBJECT_REQUIRED = True
|
|
770
|
+
|
|
771
|
+
def _get_actual_object(self) -> BaseActivity:
|
|
772
|
+
if BACKEND is None:
|
|
773
|
+
raise UninitializedBackendError
|
|
774
|
+
|
|
775
|
+
# XXX(tsileo): overrides get_object instead?
|
|
776
|
+
obj = self.get_object()
|
|
777
|
+
if (
|
|
778
|
+
obj.id.startswith(BACKEND.base_url())
|
|
779
|
+
and obj.ACTIVITY_TYPE == ActivityType.TOMBSTONE
|
|
780
|
+
):
|
|
781
|
+
obj = parse_activity(BACKEND.fetch_iri(obj.id))
|
|
782
|
+
if obj.ACTIVITY_TYPE == ActivityType.TOMBSTONE:
|
|
783
|
+
# If we already received it, we may be able to get a copy
|
|
784
|
+
better_obj = BACKEND.fetch_iri(obj.id)
|
|
785
|
+
if better_obj:
|
|
786
|
+
return parse_activity(better_obj)
|
|
787
|
+
return obj
|
|
788
|
+
|
|
789
|
+
def _recipients(self) -> List[str]:
|
|
790
|
+
obj = self._get_actual_object()
|
|
791
|
+
return obj._recipients()
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
class Update(BaseActivity):
|
|
795
|
+
ACTIVITY_TYPE = ActivityType.UPDATE
|
|
796
|
+
ALLOWED_OBJECT_TYPES = CREATE_TYPES + ACTOR_TYPES
|
|
797
|
+
OBJECT_REQUIRED = True
|
|
798
|
+
ACTOR_REQUIRED = True
|
|
799
|
+
|
|
800
|
+
def _recipients(self) -> List[str]:
|
|
801
|
+
# TODO(tsileo): audience support?
|
|
802
|
+
recipients = []
|
|
803
|
+
for field in ["to", "cc", "bto", "bcc"]:
|
|
804
|
+
if field in self._data:
|
|
805
|
+
recipients.extend(_to_list(self._data[field]))
|
|
806
|
+
|
|
807
|
+
recipients.extend(self.get_object()._recipients())
|
|
808
|
+
|
|
809
|
+
return recipients
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
class Create(BaseActivity):
|
|
813
|
+
ACTIVITY_TYPE = ActivityType.CREATE
|
|
814
|
+
ALLOWED_OBJECT_TYPES = CREATE_TYPES
|
|
815
|
+
OBJECT_REQUIRED = True
|
|
816
|
+
ACTOR_REQUIRED = True
|
|
817
|
+
|
|
818
|
+
def is_public(self) -> bool:
|
|
819
|
+
"""Returns True if the activity is addressed to the special "public" collection."""
|
|
820
|
+
for field in ["to", "cc", "bto", "bcc"]:
|
|
821
|
+
if field in self._data:
|
|
822
|
+
if AS_PUBLIC in _to_list(self._data[field]):
|
|
823
|
+
return True
|
|
824
|
+
|
|
825
|
+
return False
|
|
826
|
+
|
|
827
|
+
def _set_id(self, uri: str, obj_id: str) -> None:
|
|
828
|
+
if BACKEND is None:
|
|
829
|
+
raise UninitializedBackendError
|
|
830
|
+
|
|
831
|
+
# FIXME(tsileo): add a BACKEND.note_activity_url, and pass the actor to both
|
|
832
|
+
self._data["object"]["id"] = uri + "/activity"
|
|
833
|
+
if "url" not in self._data["object"]:
|
|
834
|
+
self._data["object"]["url"] = BACKEND.note_url(obj_id)
|
|
835
|
+
if isinstance(self.ctx(), Note):
|
|
836
|
+
try:
|
|
837
|
+
self.ctx().id = self._data["object"]["id"]
|
|
838
|
+
except NotImplementedError:
|
|
839
|
+
pass
|
|
840
|
+
self.reset_object_cache()
|
|
841
|
+
|
|
842
|
+
def _init(self) -> None:
|
|
843
|
+
obj = self.get_object()
|
|
844
|
+
if not obj.attributedTo:
|
|
845
|
+
self._data["object"]["attributedTo"] = self.get_actor().id
|
|
846
|
+
if not obj.published:
|
|
847
|
+
if self.published:
|
|
848
|
+
self._data["object"]["published"] = self.published
|
|
849
|
+
else:
|
|
850
|
+
now = format_datetime(datetime.now().astimezone())
|
|
851
|
+
self._data["published"] = now
|
|
852
|
+
self._data["object"]["published"] = now
|
|
853
|
+
|
|
854
|
+
def _recipients(self) -> List[str]:
|
|
855
|
+
# TODO(tsileo): audience support?
|
|
856
|
+
recipients = []
|
|
857
|
+
for field in ["to", "cc", "bto", "bcc"]:
|
|
858
|
+
if field in self._data:
|
|
859
|
+
recipients.extend(_to_list(self._data[field]))
|
|
860
|
+
|
|
861
|
+
recipients.extend(self.get_object()._recipients())
|
|
862
|
+
|
|
863
|
+
return recipients
|
|
864
|
+
|
|
865
|
+
def get_tombstone(self, deleted: Optional[str] = None) -> BaseActivity:
|
|
866
|
+
return Tombstone(
|
|
867
|
+
id=self.id,
|
|
868
|
+
published=self.get_object().published,
|
|
869
|
+
deleted=deleted,
|
|
870
|
+
updated=deleted,
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
class Tombstone(BaseActivity):
|
|
875
|
+
ACTIVITY_TYPE = ActivityType.TOMBSTONE
|
|
876
|
+
ACTOR_REQUIRED = False
|
|
877
|
+
OBJECT_REQUIRED = False
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
class Note(BaseActivity):
|
|
881
|
+
ACTIVITY_TYPE = ActivityType.NOTE
|
|
882
|
+
ACTOR_REQUIRED = True
|
|
883
|
+
OBJECT_REQURIED = False
|
|
884
|
+
|
|
885
|
+
def _init(self) -> None:
|
|
886
|
+
if "sensitive" not in self._data:
|
|
887
|
+
self._data["sensitive"] = False
|
|
888
|
+
|
|
889
|
+
def _recipients(self) -> List[str]:
|
|
890
|
+
# TODO(tsileo): audience support?
|
|
891
|
+
recipients: List[str] = []
|
|
892
|
+
|
|
893
|
+
for field in ["to", "cc", "bto", "bcc"]:
|
|
894
|
+
if field in self._data:
|
|
895
|
+
recipients.extend(_to_list(self._data[field]))
|
|
896
|
+
|
|
897
|
+
return recipients
|
|
898
|
+
|
|
899
|
+
def build_create(self) -> BaseActivity:
|
|
900
|
+
"""Wraps an activity in a Create activity."""
|
|
901
|
+
create_payload = {
|
|
902
|
+
"object": self.to_dict(embed=True),
|
|
903
|
+
"actor": self.attributedTo,
|
|
904
|
+
}
|
|
905
|
+
for field in ["published", "to", "bto", "cc", "bcc", "audience"]:
|
|
906
|
+
if field in self._data:
|
|
907
|
+
create_payload[field] = self._data[field]
|
|
908
|
+
|
|
909
|
+
create = Create(**create_payload)
|
|
910
|
+
create.set_ctx(self)
|
|
911
|
+
|
|
912
|
+
return create
|
|
913
|
+
|
|
914
|
+
def build_like(self, as_actor: ActorType) -> BaseActivity:
|
|
915
|
+
return Like(object=self.id, actor=as_actor.id)
|
|
916
|
+
|
|
917
|
+
def build_announce(self, as_actor: ActorType) -> BaseActivity:
|
|
918
|
+
return Announce(
|
|
919
|
+
actor=as_actor.id,
|
|
920
|
+
object=self.id,
|
|
921
|
+
to=[AS_PUBLIC],
|
|
922
|
+
cc=[as_actor.followers, self.attributedTo],
|
|
923
|
+
published=format_datetime(datetime.now().astimezone()),
|
|
924
|
+
)
|
|
925
|
+
|
|
926
|
+
def has_mention(self, actor_id: str) -> bool:
|
|
927
|
+
if self.tag is not None:
|
|
928
|
+
for tag in self.tag:
|
|
929
|
+
try:
|
|
930
|
+
if tag["type"] == ActivityType.MENTION.value:
|
|
931
|
+
if tag["href"] == actor_id:
|
|
932
|
+
return True
|
|
933
|
+
except Exception:
|
|
934
|
+
logger.exception(f"invalid tag {tag!r}")
|
|
935
|
+
|
|
936
|
+
return False
|
|
937
|
+
|
|
938
|
+
def get_in_reply_to(self) -> Optional[str]:
|
|
939
|
+
return _get_id(self.inReplyTo)
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
class Question(Note):
|
|
943
|
+
ACTIVITY_TYPE = ActivityType.QUESTION
|
|
944
|
+
ACTOR_REQUIRED = True
|
|
945
|
+
OBJECT_REQURIED = False
|
|
946
|
+
|
|
947
|
+
def one_of(self) -> List[Dict[str, Any]]:
|
|
948
|
+
return self._data.get("oneOf", [])
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
class Article(Note):
|
|
952
|
+
ACTIVITY_TYPE = ActivityType.ARTICLE
|
|
953
|
+
ACTOR_REQUIRED = True
|
|
954
|
+
OBJECT_REQURIED = False
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
class Page(Note):
|
|
958
|
+
ACTIVITY_TYPE = ActivityType.PAGE
|
|
959
|
+
ACTOR_REQUIRED = True
|
|
960
|
+
OBJECT_REQURIED = False
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
class Video(Note):
|
|
964
|
+
ACTIVITY_TYPE = ActivityType.VIDEO
|
|
965
|
+
ACTOR_REQUIRED = True
|
|
966
|
+
OBJECT_REQURIED = False
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
class Document(Note):
|
|
970
|
+
ACTIVITY_TYPE = ActivityType.DOCUMENT
|
|
971
|
+
ACTOR_REQUIRED = True
|
|
972
|
+
OBJECT_REQUIRED = False
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
class Audio(Note):
|
|
976
|
+
ACTIVITY_TYPE = ActivityType.AUDIO
|
|
977
|
+
ACTOR_REQUIRED = True
|
|
978
|
+
OBJECT_REQUIRED = False
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
def fetch_remote_activity(
|
|
982
|
+
iri: str, expected: Optional[ActivityType] = None
|
|
983
|
+
) -> BaseActivity:
|
|
984
|
+
return parse_activity(get_backend().fetch_iri(iri), expected=expected)
|