diff --git a/app/main.py b/app/main.py index 8325ada..814c650 100644 --- a/app/main.py +++ b/app/main.py @@ -73,6 +73,8 @@ from app.templates import is_current_user_admin from app.uploads import UPLOAD_DIR from app.utils import pagination from app.utils.emoji import EMOJIS_BY_NAME +from app.utils.facepile import Face +from app.utils.facepile import merge_faces from app.utils.highlight import HIGHLIGHT_CSS_HASH from app.utils.url import check_url from app.webfinger import get_remote_follow_template @@ -724,7 +726,7 @@ async def _fetch_webmentions( models.Webmention.outbox_object_id == outbox_object.id, models.Webmention.is_deleted.is_(False), ) - .limit(10) + .limit(50) ) ).all() @@ -774,9 +776,9 @@ async def outbox_by_public_id( is_current_user_admin=is_current_user_admin(request), ) + webmentions = await _fetch_webmentions(db_session, maybe_object) likes = await _fetch_likes(db_session, maybe_object) shares = await _fetch_shares(db_session, maybe_object) - webmentions = await _fetch_webmentions(db_session, maybe_object) return await templates.render_template( db_session, request, @@ -784,13 +786,52 @@ async def outbox_by_public_id( { "replies_tree": replies_tree, "outbox_object": maybe_object, - "likes": likes, - "shares": shares, - "webmentions": webmentions, + "likes": _merge_faces_from_inbox_object_and_webmentions( + likes, + webmentions, + models.WebmentionType.LIKE, + ), + "shares": _merge_faces_from_inbox_object_and_webmentions( + shares, + webmentions, + models.WebmentionType.REPOST, + ), + "webmentions": _filter_webmentions(webmentions), }, ) +def _filter_webmentions( + webmentions: list[models.Webmention], +) -> list[models.Webmention]: + return [ + wm + for wm in webmentions + if wm.webmention_type + not in [ + models.WebmentionType.LIKE, + models.WebmentionType.REPOST, + ] + ] + + +def _merge_faces_from_inbox_object_and_webmentions( + inbox_objects: list[models.InboxObject], + webmentions: list[models.Webmention], + webmention_type: models.WebmentionType, +) -> list[Face]: + wm_faces = [] + for wm in webmentions: + if wm.webmention_type != webmention_type: + continue + if face := Face.from_webmention(wm): + wm_faces.append(face) + + return merge_faces( + [Face.from_inbox_object(obj) for obj in inbox_objects] + wm_faces + ) + + @app.get("/articles/{short_id}/{slug}") async def article_by_slug( short_id: str, @@ -826,9 +867,17 @@ async def article_by_slug( { "replies_tree": replies_tree, "outbox_object": maybe_object, - "likes": likes, - "shares": shares, - "webmentions": webmentions, + "likes": _merge_faces_from_inbox_object_and_webmentions( + likes, + webmentions, + models.WebmentionType.LIKE, + ), + "shares": _merge_faces_from_inbox_object_and_webmentions( + shares, + webmentions, + models.WebmentionType.REPOST, + ), + "webmentions": _filter_webmentions(webmentions), }, ) diff --git a/app/templates/utils.html b/app/templates/utils.html index b8a749a..298747e 100644 --- a/app/templates/utils.html +++ b/app/templates/utils.html @@ -711,8 +711,8 @@
Likes
{% for like in likes %} - - {{ like.actor.handle}} + + {{ like.name }} {% endfor %} {% if object.likes_count > likes | length %} @@ -728,8 +728,8 @@
Shares
{% for share in shares %} - - {{ share.actor.handle}} + + {{ share.name }} {% endfor %} {% if object.announces_count > shares | length %} diff --git a/app/utils/facepile.py b/app/utils/facepile.py new file mode 100644 index 0000000..238b9bf --- /dev/null +++ b/app/utils/facepile.py @@ -0,0 +1,83 @@ +import datetime +from dataclasses import dataclass +from typing import Optional + +from loguru import logger + +from app import media +from app.models import InboxObject +from app.models import Webmention +from app.utils.url import make_abs + + +@dataclass +class Face: + ap_actor_id: str | None + url: str + name: str + picture_url: str + created_at: datetime.datetime + + @classmethod + def from_inbox_object(cls, like: InboxObject) -> "Face": + return cls( + ap_actor_id=like.actor.ap_id, + url=like.actor.url, # type: ignore + name=like.actor.handle, # type: ignore + picture_url=like.actor.resized_icon_url, + created_at=like.created_at, # type: ignore + ) + + @classmethod + def from_webmention(cls, webmention: Webmention) -> Optional["Face"]: + items = webmention.source_microformats.get("items", []) # type: ignore + for item in items: + if item["type"][0] == "h-card": + try: + return cls( + ap_actor_id=None, + url=webmention.source, + name=item["properties"]["name"][0], + picture_url=media.resized_media_url( + make_abs( + item["properties"]["photo"][0], webmention.source + ), # type: ignore + 50, + ), + created_at=webmention.created_at, # type: ignore + ) + except Exception: + logger.exception( + f"Failed to build Face for webmention id={webmention.id}" + ) + break + elif item["type"][0] == "h-entry": + author = item["properties"]["author"][0] + try: + return cls( + ap_actor_id=None, + url=webmention.source, + name=author["properties"]["name"][0], + picture_url=media.resized_media_url( + make_abs( + author["properties"]["photo"][0], webmention.source + ), # type: ignore + 50, + ), + created_at=webmention.created_at, # type: ignore + ) + except Exception: + logger.exception( + f"Failed to build Face for webmention id={webmention.id}" + ) + break + + return None + + +def merge_faces(faces: list[Face]) -> list[Face]: + return sorted( + faces, + key=lambda f: f.created_at, + reverse=True, + )[:10]