diff --git a/activitypub.py b/activitypub.py index 9097d05..4820eaf 100644 --- a/activitypub.py +++ b/activitypub.py @@ -20,6 +20,8 @@ from config import USERNAME from little_boxes import activitypub as ap from little_boxes.backend import Backend from little_boxes.collection import parse_collection as ap_parse_collection +from little_boxes.errors import Error + logger = logging.getLogger(__name__) @@ -41,6 +43,15 @@ def _to_list(data: Union[List[Any], Any]) -> List[Any]: return [data] +def ensure_it_is_me(f): + """Method decorator used to track the events fired during tests.""" + def wrapper(*args, **kwargs): + if args[1].id != MY_PERSON.id: + raise Error('unexpected actor') + return f(*args, **kwargs) + return wrapper + + class MicroblogPubBackend(Backend): def user_agent(self) -> str: return USER_AGENT @@ -51,6 +62,7 @@ class MicroblogPubBackend(Backend): def activity_url(self, obj_id): return f"{BASE_URL}/outbox/{obj_id}" + @ensure_it_is_me def outbox_new(self, as_actor: ap.Person, activity: ap.BaseActivity) -> None: DB.outbox.insert_one( { @@ -61,6 +73,7 @@ class MicroblogPubBackend(Backend): } ) + @ensure_it_is_me def outbox_is_blocked(self, as_actor: ap.Person, actor_id: str) -> bool: return bool( DB.outbox.find_one( @@ -73,11 +86,14 @@ class MicroblogPubBackend(Backend): ) def fetch_iri(self, iri: str) -> ap.ObjectType: - pass + # FIXME(tsileo): implements caching + return super().fetch_iri(iri) + @ensure_it_is_me def inbox_check_duplicate(self, as_actor: ap.Person, iri: str) -> bool: return bool(DB.inbox.find_one({"remote_id": iri})) + @ensure_it_is_me def inbox_new(self, as_actor: ap.Person, activity: ap.BaseActivity) -> None: DB.inbox.insert_one( { @@ -88,28 +104,34 @@ class MicroblogPubBackend(Backend): } ) + @ensure_it_is_me def post_to_remote_inbox(self, as_actor: ap.Person, payload: str, to: str) -> None: tasks.post_to_inbox.delay(payload, to) + @ensure_it_is_me def new_follower(self, as_actor: ap.Person, follow: ap.Follow) -> None: remote_actor = follow.get_actor().id if DB.followers.find({"remote_actor": remote_actor}).count() == 0: DB.followers.insert_one({"remote_actor": remote_actor}) + @ensure_it_is_me def undo_new_follower(self, as_actor: ap.Person, follow: ap.Follow) -> None: # TODO(tsileo): update the follow to set undo DB.followers.delete_one({"remote_actor": follow.get_actor().id}) + @ensure_it_is_me def undo_new_following(self, as_actor: ap.Person, follow: ap.Follow) -> None: # TODO(tsileo): update the follow to set undo DB.following.delete_one({"remote_actor": follow.get_object().id}) + @ensure_it_is_me def new_following(self, as_actor: ap.Person, follow: ap.Follow) -> None: remote_actor = follow.get_actor().id if DB.following.find({"remote_actor": remote_actor}).count() == 0: DB.following.insert_one({"remote_actor": remote_actor}) + @ensure_it_is_me def inbox_like(self, as_actor: ap.Person, like: ap.Like) -> None: obj = like.get_object() # Update the meta counter if the object is published by the server @@ -117,6 +139,7 @@ class MicroblogPubBackend(Backend): {"activity.object.id": obj.id}, {"$inc": {"meta.count_like": 1}} ) + @ensure_it_is_me def inbox_undo_like(self, as_actor: ap.Person, like: ap.Like) -> None: obj = like.get_object() # Update the meta counter if the object is published by the server @@ -124,6 +147,7 @@ class MicroblogPubBackend(Backend): {"activity.object.id": obj.id}, {"$inc": {"meta.count_like": -1}} ) + @ensure_it_is_me def outobx_like(self, as_actor: ap.Person, like: ap.Like) -> None: obj = like.get_object() # Unlikely, but an actor can like it's own post @@ -136,6 +160,7 @@ class MicroblogPubBackend(Backend): {"activity.object.id": obj.id}, {"$set": {"meta.liked": like.id}} ) + @ensure_it_is_me def outbox_undo_like(self, as_actor: ap.Person, like: ap.Like) -> None: obj = like.get_object() # Unlikely, but an actor can like it's own post @@ -147,6 +172,7 @@ class MicroblogPubBackend(Backend): {"activity.object.id": obj.id}, {"$set": {"meta.liked": False}} ) + @ensure_it_is_me def inbox_announce(self, as_actor: ap.Person, announce: ap.Announce) -> None: if isinstance(announce._data["object"], str) and not announce._data[ "object" @@ -166,6 +192,7 @@ class MicroblogPubBackend(Backend): {"activity.object.id": obj_iri}, {"$inc": {"meta.count_boost": 1}} ) + @ensure_it_is_me def inbox_undo_announce(self, as_actor: ap.Person, announce: ap.Announce) -> None: obj = announce.get_object() # Update the meta counter if the object is published by the server @@ -173,18 +200,21 @@ class MicroblogPubBackend(Backend): {"activity.object.id": obj.id}, {"$inc": {"meta.count_boost": -1}} ) + @ensure_it_is_me def outbox_announce(self, as_actor: ap.Person, announce: ap.Announce) -> None: obj = announce.get_object() DB.inbox.update_one( {"activity.object.id": obj.id}, {"$set": {"meta.boosted": announce.id}} ) + @ensure_it_is_me def outbox_undo_announce(self, as_actor: ap.Person, announce: ap.Announce) -> None: obj = announce.get_object() DB.inbox.update_one( {"activity.object.id": obj.id}, {"$set": {"meta.boosted": False}} ) + @ensure_it_is_me def inbox_delete(self, as_actor: ap.Person, delete: ap.Delete) -> None: DB.inbox.update_one( {"activity.object.id": delete.get_object().id}, @@ -197,12 +227,14 @@ class MicroblogPubBackend(Backend): # TODO(tsileo): also purge the cache if it's a reply of a published activity + @ensure_it_is_me def outbox_delete(self, as_actor: ap.Person, delete: ap.Delete) -> None: DB.outbox.update_one( {"activity.object.id": delete.get_object().id}, {"$set": {"meta.deleted": True}}, ) + @ensure_it_is_me def inbox_update(self, as_actor: ap.Person, update: ap.Update) -> None: obj = update.get_object() if obj.ACTIVITY_TYPE == ap.ActivityType.NOTE: @@ -214,6 +246,7 @@ class MicroblogPubBackend(Backend): # FIXME(tsileo): handle update actor amd inbox_update_note/inbox_update_actor + @ensure_it_is_me def outbox_update(self, as_actor: ap.Person, update: ap.Update) -> None: obj = update._data["object"] diff --git a/app.py b/app.py index e337204..78efbe7 100644 --- a/app.py +++ b/app.py @@ -38,6 +38,7 @@ from werkzeug.utils import secure_filename import activitypub import config from activitypub import embed_collection +from activitypub import MY_PERSON from config import ACTOR_SERVICE from config import ADMIN_API_KEY from config import BASE_URL @@ -55,6 +56,7 @@ from config import USERNAME from config import VERSION from config import _drop_db from config import custom_cache_purge_hook +from little_boxes import activitypub as ap from little_boxes.activitypub import ActivityType from little_boxes.activitypub import clean_activity from little_boxes.errors import BadActivityError @@ -88,6 +90,8 @@ else: SIG_AUTH = HTTPSigAuth(KEY) +OUTBOX = ap.Outbox(MY_PERSON) + def verify_pass(pwd): return bcrypt.verify(pwd, PASS) @@ -377,8 +381,9 @@ def authorize_follow(): if DB.following.find({"remote_actor": actor}).count() > 0: return redirect("/following") - follow = activitypub.Follow(object=actor) - follow.post_to_outbox() + follow = activitypub.Follow(actor=MY_PERSON, object=actor) + OUTBOX.post(follow) + return redirect("/following") @@ -400,6 +405,7 @@ def u2f_register(): ####### # Activity pub routes +# FIXME(tsileo); continue here @app.route("/") diff --git a/tasks.py b/tasks.py index 37b0257..a5c85db 100644 --- a/tasks.py +++ b/tasks.py @@ -9,18 +9,17 @@ from requests.exceptions import HTTPError from config import DB from config import HEADERS -from config import ID from config import KEY from config import USER_AGENT -from utils.httpsig import HTTPSigAuth -from utils.linked_data_sig import generate_signature +from little_boxes.httpsig import HTTPSigAuth +from little_boxes.linked_data_sig import generate_signature from utils.opengraph import fetch_og_metadata log = logging.getLogger(__name__) app = Celery( "tasks", broker=os.getenv("MICROBLOGPUB_AMQP_BROKER", "pyamqp://guest@localhost//") ) -SigAuth = HTTPSigAuth(ID + "#main-key", KEY.privkey) +SigAuth = HTTPSigAuth(KEY) @app.task(bind=True, max_retries=12) @@ -29,7 +28,7 @@ def post_to_inbox(self, payload: str, to: str) -> None: log.info("payload=%s", payload) log.info("generating sig") signed_payload = json.loads(payload) - generate_signature(signed_payload, KEY.privkey) + generate_signature(signed_payload, KEY) log.info("to=%s", to) resp = requests.post( to,