diff --git a/app/boxes.py b/app/boxes.py index 19a92f8..8ebd7e2 100644 --- a/app/boxes.py +++ b/app/boxes.py @@ -32,6 +32,8 @@ from app.config import BLOCKED_SERVERS from app.config import ID from app.config import MANUALLY_APPROVES_FOLLOWERS from app.config import set_moved_to +from app.config import stream_visibility_callback +from app.customization import ObjectInfo from app.database import AsyncSession from app.outgoing_activities import new_outgoing_activity from app.source import dedup_tags @@ -1881,16 +1883,30 @@ async def _process_note_object( is_from_following = ro.actor.ap_id in {f.ap_actor_id for f in following} is_reply = bool(ro.in_reply_to) - is_local_reply = ( + is_local_reply = bool( ro.in_reply_to and ro.in_reply_to.startswith(BASE_URL) and ro.content # Hide votes from Question ) is_mention = False + hashtags = [] tags = ro.ap_object.get("tag", []) for tag in ap.as_list(tags): if tag.get("name") == LOCAL_ACTOR.handle or tag.get("href") == LOCAL_ACTOR.url: is_mention = True + if tag.get("type") == "Hashtag": + if tag_name := tag.get("name"): + hashtags.append(tag_name) + + object_info = ObjectInfo( + is_reply=is_reply, + is_local_reply=is_local_reply, + is_mention=is_mention, + is_from_following=is_from_following, + hashtags=hashtags, + actor_handle=ro.actor.handle, + remote_object=ro, + ) inbox_object = models.InboxObject( server=urlparse(ro.ap_id).hostname, @@ -1908,9 +1924,7 @@ async def _process_note_object( activity_object_ap_id=ro.activity_object_ap_id, og_meta=await opengraph.og_meta_from_note(db_session, ro), # Hide replies from the stream - is_hidden_from_stream=not ( - (not is_reply and is_from_following) or is_mention or is_local_reply - ), + is_hidden_from_stream=not stream_visibility_callback(object_info), # We may already have some replies in DB replies_count=await _get_replies_count(db_session, ro.ap_id), ) diff --git a/app/config.py b/app/config.py index 54bd4e1..e8e2a4d 100644 --- a/app/config.py +++ b/app/config.py @@ -16,6 +16,8 @@ from loguru import logger from mistletoe import markdown # type: ignore from app.customization import _CUSTOM_ROUTES +from app.customization import _StreamVisibilityCallback +from app.customization import default_stream_visibility_callback from app.utils.emoji import _load_emojis from app.utils.version import get_version_commit @@ -262,3 +264,14 @@ def verify_csrf_token( def hmac_sha256() -> hmac.HMAC: return hmac.new(CONFIG.secret.encode(), digestmod=hashlib.sha256) + + +stream_visibility_callback: _StreamVisibilityCallback +try: + from data.stream import ( # type: ignore # noqa: F401, E501 + custom_stream_visibility_callback, + ) + + stream_visibility_callback = custom_stream_visibility_callback +except ImportError: + stream_visibility_callback = default_stream_visibility_callback diff --git a/app/customization.py b/app/customization.py index abb9062..73d2e65 100644 --- a/app/customization.py +++ b/app/customization.py @@ -1,12 +1,19 @@ +from dataclasses import dataclass from pathlib import Path +from typing import TYPE_CHECKING from typing import Any from typing import Callable from fastapi import APIRouter from fastapi import Depends from fastapi import Request +from loguru import logger from starlette.responses import JSONResponse +if TYPE_CHECKING: + from app.ap_object import RemoteObject + + _DATA_DIR = Path().parent.resolve() / "data" _Handler = Callable[..., Any] @@ -110,3 +117,38 @@ def get_custom_router() -> APIRouter | None: router.add_api_route(path, handler.handler) return router + + +@dataclass +class ObjectInfo: + # Is it a reply? + is_reply: bool + + # Is it a reply to an outbox object + is_local_reply: bool + + # Is the object mentioning the local actor + is_mention: bool + + # Is it from someone the local actor is following + is_from_following: bool + + # List of hashtags, e.g. #microblogpub + hashtags: list[str] + + # @dev@microblog.pub + actor_handle: str + + remote_object: "RemoteObject" + + +_StreamVisibilityCallback = Callable[[ObjectInfo], bool] + + +def default_stream_visibility_callback(object_info: ObjectInfo) -> bool: + logger.info(f"{object_info=}") + return ( + (not object_info.is_reply and object_info.is_from_following) + or object_info.is_mention + or object_info.is_local_reply + )