mirror of
				https://git.sr.ht/~tsileo/microblog.pub
				synced 2025-06-05 21:59:23 +02:00 
			
		
		
		
	Merge IndieWeb likes/reposts with their AP counterpart
This commit is contained in:
		
							
								
								
									
										65
									
								
								app/main.py
									
									
									
									
									
								
							
							
						
						
									
										65
									
								
								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),
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -711,8 +711,8 @@
 | 
			
		||||
            <div class="interactions-block">Likes
 | 
			
		||||
                <div class="facepile-wrapper">
 | 
			
		||||
                {% for like in likes %}
 | 
			
		||||
                    <a href="{% if is_admin %}{{ url_for("admin_profile") }}?actor_id={{ like.actor.ap_id }}{% else %}{{ like.actor.url }}{% endif %}" title="{{ like.actor.handle }}" rel="noreferrer">
 | 
			
		||||
                        <img src="{{ like.actor.resized_icon_url }}" alt="{{ like.actor.handle}}">
 | 
			
		||||
                    <a href="{% if is_admin and like.ap_actor_id %}{{ url_for("admin_profile") }}?actor_id={{ like.ap_actor_id }}{% else %}{{ like.url }}{% endif %}" title="{{ like.name }}" rel="noreferrer">
 | 
			
		||||
                        <img src="{{ like.picture_url }}" alt="{{ like.name }}">
 | 
			
		||||
                    </a>
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
                {% if object.likes_count > likes | length %}
 | 
			
		||||
@@ -728,8 +728,8 @@
 | 
			
		||||
            <div class="interactions-block">Shares
 | 
			
		||||
                <div class="facepile-wrapper">
 | 
			
		||||
                {% for share in shares %}
 | 
			
		||||
                    <a href="{% if is_admin %}{{ url_for("admin_profile") }}?actor_id={{ share.actor.ap_id }}{% else %}{{ share.actor.url }}{% endif %}" title="{{ share.actor.handle }}" rel="noreferrer">
 | 
			
		||||
                        <img src="{{ share.actor.resized_icon_url }}" alt="{{ share.actor.handle}}">
 | 
			
		||||
                    <a href="{% if is_admin and share.ap_actor_id %}{{ url_for("admin_profile") }}?actor_id={{ share.actor.ap_actor_id }}{% else %}{{ share.url }}{% endif %}" title="{{ share.name }}" rel="noreferrer">
 | 
			
		||||
                        <img src="{{ share.picture_url }}" alt="{{ share.name }}">
 | 
			
		||||
                    </a>
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
                {% if object.announces_count > shares | length %}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										83
									
								
								app/utils/facepile.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								app/utils/facepile.py
									
									
									
									
									
										Normal file
									
								
							@@ -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]
 | 
			
		||||
		Reference in New Issue
	
	Block a user