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.uploads import UPLOAD_DIR | ||||||
| from app.utils import pagination | from app.utils import pagination | ||||||
| from app.utils.emoji import EMOJIS_BY_NAME | 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.highlight import HIGHLIGHT_CSS_HASH | ||||||
| from app.utils.url import check_url | from app.utils.url import check_url | ||||||
| from app.webfinger import get_remote_follow_template | 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.outbox_object_id == outbox_object.id, | ||||||
|                 models.Webmention.is_deleted.is_(False), |                 models.Webmention.is_deleted.is_(False), | ||||||
|             ) |             ) | ||||||
|             .limit(10) |             .limit(50) | ||||||
|         ) |         ) | ||||||
|     ).all() |     ).all() | ||||||
|  |  | ||||||
| @@ -774,9 +776,9 @@ async def outbox_by_public_id( | |||||||
|         is_current_user_admin=is_current_user_admin(request), |         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) |     likes = await _fetch_likes(db_session, maybe_object) | ||||||
|     shares = await _fetch_shares(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( |     return await templates.render_template( | ||||||
|         db_session, |         db_session, | ||||||
|         request, |         request, | ||||||
| @@ -784,13 +786,52 @@ async def outbox_by_public_id( | |||||||
|         { |         { | ||||||
|             "replies_tree": replies_tree, |             "replies_tree": replies_tree, | ||||||
|             "outbox_object": maybe_object, |             "outbox_object": maybe_object, | ||||||
|             "likes": likes, |             "likes": _merge_faces_from_inbox_object_and_webmentions( | ||||||
|             "shares": shares, |                 likes, | ||||||
|             "webmentions": webmentions, |                 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}") | @app.get("/articles/{short_id}/{slug}") | ||||||
| async def article_by_slug( | async def article_by_slug( | ||||||
|     short_id: str, |     short_id: str, | ||||||
| @@ -826,9 +867,17 @@ async def article_by_slug( | |||||||
|         { |         { | ||||||
|             "replies_tree": replies_tree, |             "replies_tree": replies_tree, | ||||||
|             "outbox_object": maybe_object, |             "outbox_object": maybe_object, | ||||||
|             "likes": likes, |             "likes": _merge_faces_from_inbox_object_and_webmentions( | ||||||
|             "shares": shares, |                 likes, | ||||||
|             "webmentions": webmentions, |                 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="interactions-block">Likes | ||||||
|                 <div class="facepile-wrapper"> |                 <div class="facepile-wrapper"> | ||||||
|                 {% for like in likes %} |                 {% 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"> |                     <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.actor.resized_icon_url }}" alt="{{ like.actor.handle}}"> |                         <img src="{{ like.picture_url }}" alt="{{ like.name }}"> | ||||||
|                     </a> |                     </a> | ||||||
|                 {% endfor %} |                 {% endfor %} | ||||||
|                 {% if object.likes_count > likes | length %} |                 {% if object.likes_count > likes | length %} | ||||||
| @@ -728,8 +728,8 @@ | |||||||
|             <div class="interactions-block">Shares |             <div class="interactions-block">Shares | ||||||
|                 <div class="facepile-wrapper"> |                 <div class="facepile-wrapper"> | ||||||
|                 {% for share in shares %} |                 {% 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"> |                     <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.actor.resized_icon_url }}" alt="{{ share.actor.handle}}"> |                         <img src="{{ share.picture_url }}" alt="{{ share.name }}"> | ||||||
|                     </a> |                     </a> | ||||||
|                 {% endfor %} |                 {% endfor %} | ||||||
|                 {% if object.announces_count > shares | length %} |                 {% 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