def save(box: Box, activity: ap.BaseActivity) -> None: """Custom helper for saving an activity to the DB.""" # Set some "type"-related neta meta = _meta(activity) if box == Box.OUTBOX and activity.has_type(ap.ActivityType.FOLLOW): meta[MetaKey.FOLLOW_STATUS.value] = FollowStatus.WAITING.value elif activity.has_type(ap.ActivityType.CREATE): mentions = [] obj = activity.get_object() for m in obj.get_mentions(): mentions.append(m.href) hashtags = [] for h in obj.get_hashtags(): hashtags.append(h.name[1:]) # Strip the # meta.update( {MetaKey.MENTIONS.value: mentions, MetaKey.HASHTAGS.value: hashtags} ) DB.activities.insert_one( { "box": box.value, "activity": activity.to_dict(), "type": _to_list(activity.type), "remote_id": activity.id, "meta": meta, } )
def _meta(activity: ap.BaseActivity) -> _NewMeta: visibility = ap.get_visibility(activity) is_public = False if visibility in [ap.Visibility.PUBLIC, ap.Visibility.UNLISTED]: is_public = True object_id = None try: object_id = activity.get_object_id() except Exception: # TODO(tsileo): should be ValueError, but replies trigger a KeyError on object pass object_visibility = None if activity.has_type( [ap.ActivityType.CREATE, ap.ActivityType.ANNOUNCE, ap.ActivityType.LIKE] ): object_visibility = ap.get_visibility(activity.get_object()).name actor_id = activity.get_actor().id return { MetaKey.UNDO.value: False, MetaKey.DELETED.value: False, MetaKey.PUBLIC.value: is_public, MetaKey.SERVER.value: urlparse(activity.id).netloc, MetaKey.VISIBILITY.value: visibility.name, MetaKey.ACTOR_ID.value: actor_id, MetaKey.OBJECT_ID.value: object_id, MetaKey.OBJECT_VISIBILITY.value: object_visibility, MetaKey.POLL_ANSWER.value: False, MetaKey.PUBLISHED.value: activity.published if activity.published else now(), }
def update_cached_actor(actor: ap.BaseActivity) -> None: actor_hash = _actor_hash(actor) update_many_activities( { **flag(MetaKey.ACTOR_ID, actor.id), **flag(MetaKey.ACTOR_HASH, {"$ne": actor_hash}), }, upsert({ MetaKey.ACTOR: actor.to_dict(embed=True), MetaKey.ACTOR_HASH: actor_hash }), ) update_many_activities( { **flag(MetaKey.OBJECT_ACTOR_ID, actor.id), **flag(MetaKey.OBJECT_ACTOR_HASH, {"$ne": actor_hash}), }, upsert({ MetaKey.OBJECT_ACTOR: actor.to_dict(embed=True), MetaKey.OBJECT_ACTOR_HASH: actor_hash, }), ) # TODO(tsileo): Also update following (it's in the object) # DB.activities.update_many( # {"meta.object_id": actor.id}, {"$set": {"meta.object": actor.to_dict(embed=True)}} # ) _cache_actor_icon(actor)
def post_to_outbox(activity: ap.BaseActivity) -> str: if activity.has_type(ap.CREATE_TYPES): activity = activity.build_create() # Assign create a random ID obj_id = back.random_object_id() activity.set_id(back.activity_url(obj_id), obj_id) back.save(Box.OUTBOX, activity) cache_actor.delay(activity.id) finish_post_to_outbox.delay(activity.id) return activity.id
def outbox_new(self, as_actor: ap.Person, activity: ap.BaseActivity) -> None: print(f"saving {activity!r} to DB") actor_id = activity.get_actor().id if activity.id in self.OUTBOX_IDX[actor_id]: return self.DB[actor_id]["outbox"].append(activity) self.OUTBOX_IDX[actor_id][activity.id] = activity self.FETCH_MOCK[activity.id] = activity.to_dict() if isinstance(activity, ap.Create): self.FETCH_MOCK[ activity.get_object().id] = activity.get_object().to_dict()
def update_remote_actor(actor_id: int, activity_actor: ap.BaseActivity) -> None: """ :param actor_id: an Actor db ID :param activity_actor: a Little Boxes Actor object :return: nothing """ actor = Actor.query.filter(Actor.id == actor_id).first() current_app.logger.debug( f"asked to update Actor {actor_id}: {activity_actor!r}") actor.preferred_username = activity_actor.preferredUsername domain = urlparse(activity_actor.url) actor.domain = domain.netloc actor.name = activity_actor.name actor.manually_approves_followers = False actor.url = activity_actor.id # FIXME: or .id ??? [cf backend.py:52-53] actor.shared_inbox_url = activity_actor._data.get("endpoints", {}).get("sharedInbox") actor.inbox_url = activity_actor.inbox actor.outbox_url = activity_actor.outbox actor.public_key = activity_actor.get_key().pubkey_pem actor.summary = activity_actor.summary actor.followers_url = activity_actor.followers actor.following_url = activity_actor.following db.session.commit()
def cache_emojis(activity: ap.BaseActivity) -> None: for emoji in activity.get_emojis(): try: Tasks.cache_emoji(emoji.get_icon_url(), emoji.id) except KeyError: # TODO(tsileo): log invalid emoji pass
def post_to_outbox(activity: ap.BaseActivity) -> str: current_app.logger.debug(f"post_to_outbox {activity}") if activity.has_type(ap.CREATE_TYPES): activity = activity.build_create() backend = ap.get_backend() # Assign a random ID obj_id = backend.random_object_id() activity.set_id(backend.activity_url(obj_id), obj_id) backend.save(Box.OUTBOX, activity) finish_post_to_outbox.delay(activity.id) return activity.id
def create_remote_actor(activity_actor: ap.BaseActivity): """ :param activity_actor: a Little Boxes Actor object :return: an Actor object """ actor = Actor() print(activity_actor) actor.preferred_username = activity_actor.preferredUsername domain = urlparse(activity_actor.url) actor.domain = domain.netloc actor.type = "Person" # FIXME: test for .name, it won't exist if not set (at least for mastodon) actor.name = activity_actor.preferredUsername # mastodon don't have .name actor.manually_approves_followers = False actor.url = activity_actor.id # FIXME: or .id ??? [cf backend.py:52-53] actor.shared_inbox_url = activity_actor._data.get("endpoints", {}).get("sharedInbox") actor.inbox_url = activity_actor.inbox actor.outbox_url = activity_actor.outbox actor.public_key = activity_actor.get_key().pubkey_pem actor.summary = activity_actor.summary actor.followers_url = activity_actor.followers actor.following_url = activity_actor.following return actor
def create_remote_actor(activity_actor: ap.BaseActivity): """ :param activity_actor: a Little Boxes Actor object :return: an Actor object """ actor = Actor() actor.preferred_username = activity_actor.preferredUsername domain = urlparse(activity_actor.url) actor.domain = domain.netloc actor.type = "Person" actor.name = activity_actor.name actor.manually_approves_followers = False actor.url = activity_actor.id # FIXME: or .id ??? [cf backend.py:52-53] actor.shared_inbox_url = activity_actor._data.get("endpoints", {}).get("sharedInbox") actor.inbox_url = activity_actor.inbox actor.outbox_url = activity_actor.outbox actor.public_key = activity_actor.get_key().pubkey_pem actor.summary = activity_actor.summary actor.followers_url = activity_actor.followers actor.following_url = activity_actor.following user = User() user.email = None user.name = activity_actor.preferredUsername user.password = None user.active = False user.confirmed_at = None user.display_name = activity_actor.name user.local = False actor.user = user # TODO: Avatar return actor, user
def save(self, box: Box, activity: ap.BaseActivity) -> None: """Save an Activity in database""" current_app.logger.info(f"asked to save an activity {activity!r}") # Save remote Actor ap_actor = activity.get_actor() domain = urlparse(ap_actor.id) current_app.logger.debug(f"actor.id=={ap_actor.__dict__}") current_app.logger.debug(f"actor domain {domain.netloc} and " f"name {ap_actor.preferredUsername}") actor = Actor.query.filter(Actor.domain == domain.netloc, Actor.name == ap_actor.preferredUsername).first() # FIXME TODO: check if it still works with unknown remote actor if not actor: current_app.logger.debug("cannot find actor") actor = Actor.query.filter(Actor.url == ap_actor.id).first() if not actor: current_app.logger.debug(f"actor {ap_actor.id} not found") actor, user = create_remote_actor(ap_actor) db.session.add(user) db.session.add(actor) current_app.logger.debug("created one in DB") else: current_app.logger.debug(f"got local one {actor.url}") else: current_app.logger.debug(f"got remote one {actor.url}") # Save Activity act = Activity() act.payload = activity.to_dict() act.url = activity.id act.type = activity.type act.box = box.value # Activity is local only if the url starts like BASE_URL base_url = current_app.config["BASE_URL"] act.local = activity.id.startswith(base_url) act.actor_id = actor.id db.session.add(act) db.session.commit()
def save(self, box: Box, activity: ap.BaseActivity) -> None: """Custom helper for saving an activity to the DB.""" is_public = True if activity.has_type( ap.ActivityType.CREATE) and not activity.is_public(): is_public = False DB.activities.insert_one({ "box": box.value, "activity": activity.to_dict(), "type": _to_list(activity.type), "remote_id": activity.id, "meta": { "undo": False, "deleted": False, "public": is_public }, })
def post_to_outbox(activity: ap.BaseActivity) -> str: current_app.logger.debug(f"post_to_outbox {activity!r}") if activity.has_type(ap.CREATE_TYPES): print("BUILD CREATE POST TO OUTBOX") activity = activity.build_create() backend = ap.get_backend() # Assign a random ID obj_id = backend.random_object_id() activity.set_id(backend.activity_url(obj_id), obj_id) backend.save(Box.OUTBOX, activity) # Broadcast only if AP is enabled if current_app.config["AP_ENABLED"]: finish_post_to_outbox.delay(activity.id) return activity.id
def outbox_delete(self, as_actor: ap.Person, activity: ap.BaseActivity) -> None: current_app.logger.debug(f"outbox_delete {activity!r} as {as_actor!r}") # Fetch linked activity and mark it deleted # Somehow we needs to remove /activity here # FIXME do that better activity_uri = activity.get_object_id().rstrip("/activity") current_app.logger.debug(f"id: {activity_uri}") orig_activity = Activity.query.filter(Activity.url == activity_uri, Activity.type == "Create").first() orig_activity.meta_deleted = True db.session.commit()
def accept_follow(activity: ap.BaseActivity) -> str: actor_id = activity.get_actor().id accept = ap.Accept( actor=ID, context=new_context(activity), object={ "type": "Follow", "id": activity.id, "object": activity.get_object_id(), "actor": actor_id, }, to=[actor_id], published=now(), ) update_one_activity( by_remote_id(activity.id), upsert({MetaKey.FOLLOW_STATUS: FollowStatus.ACCEPTED.value}), ) return post_to_outbox(accept)
def save(self, box: Box, activity: ap.BaseActivity) -> None: """Custom helper for saving an activity to the DB.""" DB.activities.insert_one( { "box": box.value, "activity": activity.to_dict(), "type": _to_list(activity.type), "remote_id": activity.id, "meta": {"undo": False, "deleted": False}, } )
def new_following(self, activity: ap.BaseActivity, obj: ap.BaseActivity) -> None: current_app.logger.info("new following") ap_from = obj.get_actor() # Who initiated the follow ap_to = activity.get_actor() # who to be followed db_from = Actor.query.filter(Actor.url == ap_from.id).first() db_to = Actor.query.filter(Actor.url == ap_to.id).first() if not db_from: current_app.logger.error(f"cannot find actor {ap_from!r}") return if not db_to: current_app.logger.error(f"cannot find follow {ap_to!r}") return current_app.logger.info(f"{db_from.name} wanted to follow {db_to.name}") # FIXME: may be the reverse, db_follow follow db_actor db_from.follow(activity.id, db_to) db.session.commit() current_app.logger.info("new following saved")
def save_reply(activity: ap.BaseActivity, meta: Dict[str, Any] = {}) -> None: visibility = ap.get_visibility(activity) is_public = False if visibility in [ap.Visibility.PUBLIC, ap.Visibility.UNLISTED]: is_public = True published = activity.published if activity.published else now() DB.replies.insert_one({ "activity": activity.to_dict(), "type": _to_list(activity.type), "remote_id": activity.id, "meta": { "undo": False, "deleted": False, "public": is_public, "server": urlparse(activity.id).netloc, "visibility": visibility.name, "actor_id": activity.get_actor().id, MetaKey.PUBLISHED.value: published, **meta, }, })
def save(box: Box, activity: ap.BaseActivity) -> None: """Custom helper for saving an activity to the DB.""" visibility = ap.get_visibility(activity) is_public = False if visibility in [ap.Visibility.PUBLIC, ap.Visibility.UNLISTED]: is_public = True object_id = None try: object_id = activity.get_object_id() except Exception: # TODO(tsileo): should be ValueError, but replies trigger a KeyError on object pass object_visibility = None if activity.has_type([ ap.ActivityType.CREATE, ap.ActivityType.ANNOUNCE, ap.ActivityType.LIKE ]): object_visibility = ap.get_visibility(activity.get_object()).name actor_id = activity.get_actor().id DB.activities.insert_one({ "box": box.value, "activity": activity.to_dict(), "type": _to_list(activity.type), "remote_id": activity.id, "meta": { "undo": False, "deleted": False, "public": is_public, "server": urlparse(activity.id).netloc, "visibility": visibility.name, "actor_id": actor_id, "object_id": object_id, "object_visibility": object_visibility, "poll_answer": False, }, })
def post_to_inbox(activity: ap.BaseActivity) -> None: # Check for Block activity actor = activity.get_actor() if outbox_is_blocked(actor.id): logger.info( f"actor {actor!r} is blocked, dropping the received activity {activity!r}" ) return if DB.activities.find_one({ "box": Box.INBOX.value, "remote_id": activity.id }): # The activity is already in the inbox logger.info(f"received duplicate activity {activity!r}, dropping it") return save(Box.INBOX, activity) logger.info(f"spawning tasks for {activity!r}") if not activity.has_type(ap.ActivityType.DELETE): Tasks.cache_actor(activity.id) Tasks.process_new_activity(activity.id) Tasks.finish_post_to_inbox(activity.id)
def post_to_outbox(activity: ap.BaseActivity) -> str: if activity.has_type(ap.CREATE_TYPES): activity = activity.build_create() # Assign create a random ID obj_id = binascii.hexlify(os.urandom(8)).decode("utf-8") uri = activity_url(obj_id) activity._data["id"] = uri if activity.has_type(ap.ActivityType.CREATE): activity._data["object"]["id"] = urljoin( BASE_URL, url_for("outbox_activity", item_id=obj_id)) activity._data["object"]["url"] = urljoin( BASE_URL, url_for("note_by_id", note_id=obj_id)) activity.reset_object_cache() save(Box.OUTBOX, activity) Tasks.cache_actor(activity.id) Tasks.finish_post_to_outbox(activity.id) return activity.id
def post_to_inbox(activity: ap.BaseActivity) -> None: # Check for Block activity actor = activity.get_actor() if back.outbox_is_blocked(MY_PERSON, actor.id): log.info( f"actor {actor!r} is blocked, dropping the received activity {activity!r}" ) return if back.inbox_check_duplicate(MY_PERSON, activity.id): # The activity is already in the inbox log.info(f"received duplicate activity {activity!r}, dropping it") back.save(Box.INBOX, activity) process_new_activity.delay(activity.id) log.info(f"spawning task for {activity!r}") finish_post_to_inbox.delay(activity.id)
def create_remote_actor(activity_actor: ap.BaseActivity): """ :param activity_actor: a Little Boxes Actor object :return: an Actor object """ actor = Actor() actor.preferred_username = activity_actor.preferredUsername # TODO this should be handled better, like with pyld... # handle funkwhale Actor types url = activity_actor.url if isinstance(url, list): url = next( iter(item for item in url if item["type"] == "Link" and item["mediaType"] == "text/html"), None)["href"] domain = urlparse(url) actor.domain = domain.netloc actor.type = "Person" actor.name = activity_actor.name actor.manually_approves_followers = False actor.url = activity_actor.id # FIXME: or .id ??? [cf backend.py:52-53] actor.shared_inbox_url = activity_actor._data.get("endpoints", {}).get("sharedInbox") actor.inbox_url = activity_actor.inbox actor.outbox_url = activity_actor.outbox actor.public_key = activity_actor.get_key().pubkey_pem actor.summary = activity_actor.summary actor.followers_url = activity_actor.followers actor.following_url = activity_actor.following user = User() user.email = None user.name = activity_actor.preferredUsername user.password = None user.active = False user.confirmed_at = None user.display_name = activity_actor.name user.local = False actor.user = user # TODO: Avatar return actor, user
def post_to_outbox(self, activity: ap.BaseActivity) -> None: if activity.has_type(ap.CREATE_TYPES): activity = activity.build_create() self.save(Box.OUTBOX, activity) # Assign create a random ID obj_id = self.random_object_id() activity.set_id(self.activity_url(obj_id), obj_id) recipients = activity.recipients() logger.info(f"recipients={recipients}") activity = ap.clean_activity(activity.to_dict()) payload = json.dumps(activity) for recp in recipients: logger.debug(f"posting to {recp}") self.post_to_remote_inbox(self.get_actor(), payload, recp)
def post_to_inbox(activity: ap.BaseActivity) -> None: # Check for Block activity actor = activity.get_actor() if outbox_is_blocked(actor.id): logger.info( f"actor {actor!r} is blocked, dropping the received activity {activity!r}" ) return # If the message is coming from a Pleroma relay, we process it as a possible reply for a stream activity if ( actor.has_type(ap.ActivityType.APPLICATION) and actor.id.endswith("/relay") and activity.has_type(ap.ActivityType.ANNOUNCE) and not find_one_activity( { **by_object_id(activity.get_object_id()), **by_type(ap.ActivityType.CREATE), } ) and not DB.replies.find_one(by_remote_id(activity.get_object_id())) ): Tasks.process_reply(activity.get_object_id()) return # Hubzilla sends Update with the same ID as the actor, and it poisons the cache if ( activity.has_type(ap.ActivityType.UPDATE) and activity.id == activity.get_object_id() ): # Start a task to update the cached actor Tasks.cache_actor(activity.id) return # Honk forwards activities in a Read, process them as replies if activity.has_type(ap.ActivityType.READ): Tasks.process_reply(activity.get_object_id()) return # TODO(tsileo): support ignore from Honk # Hubzilla forwards activities in a Create, process them as possible replies if activity.has_type(ap.ActivityType.CREATE) and server(activity.id) != server( activity.get_object_id() ): Tasks.process_reply(activity.get_object_id()) return if DB.activities.find_one({"box": Box.INBOX.value, "remote_id": activity.id}): # The activity is already in the inbox logger.info(f"received duplicate activity {activity!r}, dropping it") return save(Box.INBOX, activity) logger.info(f"spawning tasks for {activity!r}") if not activity.has_type([ap.ActivityType.DELETE, ap.ActivityType.UPDATE]): Tasks.cache_actor(activity.id) Tasks.process_new_activity(activity.id) Tasks.finish_post_to_inbox(activity.id)