forked from forks/microblog.pub
Display webmentions on notes
This commit is contained in:
parent
339757ebd2
commit
54334e1667
3 changed files with 111 additions and 9 deletions
|
@ -22,7 +22,7 @@
|
|||
{% macro display_replies_tree(replies_tree_node) %}
|
||||
|
||||
{% if replies_tree_node.is_requested %}
|
||||
{{ utils.display_object(replies_tree_node.ap_object, likes=likes, shares=shares, expanded=not replies_tree_node.is_root) }}
|
||||
{{ utils.display_object(replies_tree_node.ap_object, likes=likes, shares=shares, webmentions=replies_tree_node.ap_object.webmentions or [], expanded=not replies_tree_node.is_root) }}
|
||||
{% else %}
|
||||
{{ utils.display_object(replies_tree_node.ap_object) }}
|
||||
{% endif %}
|
||||
|
|
|
@ -234,7 +234,7 @@
|
|||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro display_object_expanded(object, likes=[], shares=[]) %}
|
||||
{% macro display_object_expanded(object, likes=[], shares=[], webmentions=[]) %}
|
||||
|
||||
<div class="activity-expanded h-entry">
|
||||
|
||||
|
@ -307,6 +307,18 @@
|
|||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if webmentions %}
|
||||
<div style="flex: 0 1 50%;max-width: 50%;">Webmentions
|
||||
<div style="display: flex;column-gap: 20px;row-gap:20px;flex-wrap: wrap;margin-top:20px;">
|
||||
{% for webmention in webmentions %}
|
||||
<a href="{{ webmention.url }}" style="height:50px;" rel="noreferrer">
|
||||
<img src="{{ webmention.actor_icon_url | media_proxy_url }}" alt="{{ webmention.actor_name }}" style="max-width:50px;">
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
@ -315,7 +327,7 @@
|
|||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro display_object(object, likes=[], shares=[], expanded=False) %}
|
||||
{% macro display_object(object, likes=[], shares=[], webmentions=[], expanded=False) %}
|
||||
{% if object.ap_type in ["Note", "Article", "Video"] %}
|
||||
<div class="ap-object {% if expanded %}ap-object-expanded {% endif %}h-entry" id="{{ object.permalink_id }}">
|
||||
{{ display_actor(object.actor, {}, embedded=True) }}
|
||||
|
@ -375,6 +387,13 @@
|
|||
<a href="{{ object.url }}"><strong>{{ object.announces_count }}</strong> share{{ object.announces_count | pluralize }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if object.webmentions %}
|
||||
<li>
|
||||
<a href="{{ object.url }}"><strong>{{ object.webmentions | length }}</strong> webmention{{ object.webmentions | length | pluralize }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% if (object.is_from_outbox or is_admin) and object.replies_count %}
|
||||
|
@ -442,10 +461,10 @@
|
|||
{% endif %}
|
||||
|
||||
|
||||
{% if likes or shares %}
|
||||
<div style="display: flex;column-gap: 20px;margin-top:20px;">
|
||||
{% if likes or shares or webmentions %}
|
||||
<div style="display: flex;column-gap: 20px;flex-wrap: wrap;margin-top:20px;">
|
||||
{% if likes %}
|
||||
<div style="flex: 0 1 50%;max-width: 50%;">Likes
|
||||
<div style="flex: 0 1 30%;max-width: 50%;">Likes
|
||||
<div style="display: flex;column-gap: 20px;row-gap:20px;flex-wrap: wrap;margin-top:20px;">
|
||||
{% 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 }}" style="height:50px;" rel="noreferrer">
|
||||
|
@ -457,7 +476,7 @@
|
|||
{% endif %}
|
||||
|
||||
{% if shares %}
|
||||
<div style="flex: 0 1 50%;max-width: 50%;">Shares
|
||||
<div style="flex: 0 1 30%;max-width: 50%;">Shares
|
||||
<div style="display: flex;column-gap: 20px;row-gap:20px;flex-wrap: wrap;margin-top:20px;">
|
||||
{% 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 }}" style="height:50px;" rel="noreferrer">
|
||||
|
@ -467,6 +486,19 @@
|
|||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if webmentions %}
|
||||
<div style="flex: 0 1 30%;max-width: 50%;">Webmentions
|
||||
<div style="display: flex;column-gap: 20px;row-gap:20px;flex-wrap: wrap;margin-top:20px;">
|
||||
{% for webmention in webmentions %}
|
||||
<a href="{{ webmention.url }}" title="{{ webmention.actor_name }}" style="height:50px;" rel="noreferrer">
|
||||
<img src="{{ webmention.actor_icon_url | media_proxy_url }}" alt="{{ webmention.actor_name }}" style="max-width:50px;">
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
|
|
@ -1,17 +1,63 @@
|
|||
from dataclasses import asdict
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from typing import Optional
|
||||
|
||||
from bs4 import BeautifulSoup # type: ignore
|
||||
from fastapi import APIRouter
|
||||
from fastapi import Depends
|
||||
from fastapi import HTTPException
|
||||
from fastapi import Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from loguru import logger
|
||||
|
||||
from app.boxes import get_outbox_object_by_ap_id
|
||||
from app.database import AsyncSession
|
||||
from app.database import get_db_session
|
||||
from app.database import now
|
||||
from app.utils import microformats
|
||||
from app.utils.url import check_url
|
||||
from app.utils.url import is_url_valid
|
||||
from app.utils.url import make_abs
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Webmention:
|
||||
actor_icon_url: str
|
||||
actor_name: str
|
||||
url: str
|
||||
received_at: str
|
||||
|
||||
@classmethod
|
||||
def from_microformats(
|
||||
cls, items: list[dict[str, Any]], url: str
|
||||
) -> Optional["Webmention"]:
|
||||
for item in items:
|
||||
if item["type"][0] == "h-card":
|
||||
return cls(
|
||||
actor_icon_url=make_abs(
|
||||
item["properties"]["photo"][0], url
|
||||
), # type: ignore
|
||||
actor_name=item["properties"]["name"][0],
|
||||
url=url,
|
||||
received_at=now().isoformat(),
|
||||
)
|
||||
if item["type"][0] == "h-entry":
|
||||
author = item["properties"]["author"][0]
|
||||
return cls(
|
||||
actor_icon_url=make_abs(
|
||||
author["properties"]["photo"][0], url
|
||||
), # type: ignore
|
||||
actor_name=author["properties"]["name"][0],
|
||||
url=url,
|
||||
received_at=now().isoformat(),
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def is_source_containing_target(source_html: str, target_url: str) -> bool:
|
||||
soup = BeautifulSoup(source_html, "html5lib")
|
||||
for link in soup.find_all("a"):
|
||||
|
@ -28,6 +74,7 @@ def is_source_containing_target(source_html: str, target_url: str) -> bool:
|
|||
@router.post("/webmentions")
|
||||
async def webmention_endpoint(
|
||||
request: Request,
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> JSONResponse:
|
||||
form_data = await request.form()
|
||||
try:
|
||||
|
@ -45,7 +92,11 @@ async def webmention_endpoint(
|
|||
|
||||
logger.info(f"Received webmention {source=} {target=}")
|
||||
|
||||
# TODO: get outbox via ap_id (URL is the same as ap_id)
|
||||
mentioned_object = await get_outbox_object_by_ap_id(db_session, target)
|
||||
if not mentioned_object:
|
||||
logger.info(f"Invalid target {target=}")
|
||||
raise HTTPException(status_code=400, detail="Invalid target")
|
||||
|
||||
maybe_data_and_html = await microformats.fetch_and_parse(source)
|
||||
if not maybe_data_and_html:
|
||||
logger.info("failed to fetch source")
|
||||
|
@ -57,6 +108,25 @@ async def webmention_endpoint(
|
|||
logger.warning("target not found in source")
|
||||
raise HTTPException(status_code=400, detail="target not found in source")
|
||||
|
||||
logger.info(f"{data=}")
|
||||
try:
|
||||
webmention = Webmention.from_microformats(data["items"], source)
|
||||
if not webmention:
|
||||
raise ValueError("Failed to fetch target data")
|
||||
except Exception:
|
||||
logger.warning("Failed build Webmention for {source=} with {data=}")
|
||||
return JSONResponse(content={}, status_code=200)
|
||||
|
||||
logger.info(f"{webmention=}")
|
||||
|
||||
if mentioned_object.webmentions is None:
|
||||
mentioned_object.webmentions = [asdict(webmention)]
|
||||
else:
|
||||
mentioned_object.webmentions = [asdict(webmention)] + [
|
||||
wm # type: ignore
|
||||
for wm in mentioned_object.webmentions # type: ignore
|
||||
if wm["url"] != source # type: ignore
|
||||
]
|
||||
|
||||
await db_session.commit()
|
||||
|
||||
return JSONResponse(content={}, status_code=200)
|
||||
|
|
Loading…
Reference in a new issue