diff --git a/blueprints/api.py b/blueprints/api.py index ae4447c..b90d40a 100644 --- a/blueprints/api.py +++ b/blueprints/api.py @@ -32,8 +32,8 @@ from config import MEDIA_CACHE from config import _drop_db from core import feed from core.activitypub import activity_url -from core.activitypub import post_to_outbox from core.activitypub import new_context +from core.activitypub import post_to_outbox from core.meta import Box from core.meta import MetaKey from core.meta import _meta diff --git a/blueprints/tasks.py b/blueprints/tasks.py index ce38180..0e0e8d6 100644 --- a/blueprints/tasks.py +++ b/blueprints/tasks.py @@ -24,6 +24,7 @@ from core.activitypub import Box from core.activitypub import _actor_hash from core.activitypub import _add_answers_to_question from core.activitypub import _cache_actor_icon +from core.activitypub import is_from_outbox from core.activitypub import post_to_outbox from core.activitypub import save_reply from core.activitypub import update_cached_actor @@ -48,6 +49,7 @@ from core.shared import p from core.tasks import Tasks from utils import opengraph from utils.media import is_video +from utils.webmentions import discover_webmention_endpoint blueprint = flask.Blueprint("tasks", __name__) @@ -305,6 +307,41 @@ def task_cache_attachment() -> _Response: return "" +@blueprint.route("/task/send_webmention", methods=["POST"]) +def task_send_webmention() -> _Response: + task = p.parse(flask.request) + app.logger.info(f"task={task!r}") + note_url = task.payload["note_url"] + link = task.payload["link"] + remote_id = task.payload["remote_id"] + try: + app.logger.info(f"trying to send webmention source={note_url} target={link}") + webmention_endpoint = discover_webmention_endpoint(link) + if not webmention_endpoint: + app.logger.info("no webmention endpoint") + return "" + + resp = requests.post( + webmention_endpoint, + data={"source": note_url, "target": link}, + headers={"User-Agent": config.USER_AGENT}, + ) + app.logger.info(f"webmention endpoint resp={resp}/{resp.text}") + resp.raise_for_status() + except HTTPError as err: + app.logger.exception("request failed") + if 400 >= err.response.status_code >= 499: + app.logger.info("client error, no retry") + return "" + + raise TaskError() from err + except Exception as err: + app.logger.exception(f"failed to cache actor for {link}/{remote_id}/{note_url}") + raise TaskError() from err + + return "" + + @blueprint.route("/task/cache_actor", methods=["POST"]) def task_cache_actor() -> _Response: task = p.parse(flask.request) @@ -319,10 +356,18 @@ def task_cache_actor() -> _Response: # Fetch the Open Grah metadata if it's a `Create` if activity.has_type(ap.ActivityType.CREATE): - links = opengraph.links_from_note(activity.get_object().to_dict()) + obj = activity.get_object() + links = opengraph.links_from_note(obj.to_dict()) if links: Tasks.fetch_og_meta(iri) + # Send Webmentions only if it's from the outbox, and public + if ( + is_from_outbox(obj) + and ap.get_visibility(obj) == ap.Visibility.PUBLIC + ): + Tasks.send_webmentions(activity, links) + if activity.has_type(ap.ActivityType.FOLLOW): if actor.id == config.ID: # It's a new following, cache the "object" (which is the actor we follow) diff --git a/config.py b/config.py index 18ce40d..4541d28 100644 --- a/config.py +++ b/config.py @@ -6,11 +6,11 @@ from enum import Enum from pathlib import Path import yaml +from bleach import linkify from itsdangerous import JSONWebSignatureSerializer from little_boxes import strtobool from little_boxes.activitypub import CTX_AS as AP_DEFAULT_CTX from pymongo import MongoClient -from bleach import linkify import sass from utils.emojis import _load_emojis diff --git a/core/inbox.py b/core/inbox.py index fc97ee9..334e65b 100644 --- a/core/inbox.py +++ b/core/inbox.py @@ -9,6 +9,7 @@ from little_boxes.errors import NotAnActivityError import config from core.activitypub import _answer_key from core.activitypub import handle_replies +from core.activitypub import new_context from core.activitypub import post_to_outbox from core.activitypub import update_cached_actor from core.db import DB @@ -163,6 +164,7 @@ def _follow_process_inbox(activity: ap.Follow, new_meta: _NewMeta) -> None: actor_id = activity.get_actor().id accept = ap.Accept( actor=config.ID, + context=new_context(activity), object={ "type": "Follow", "id": activity.id, diff --git a/core/tasks.py b/core/tasks.py index 036c58d..2d295d6 100644 --- a/core/tasks.py +++ b/core/tasks.py @@ -3,6 +3,7 @@ from datetime import datetime from datetime import timezone from typing import Any from typing import Dict +from typing import Set from little_boxes import activitypub as ap from poussetaches import PousseTaches @@ -40,6 +41,18 @@ class Tasks: p.push({"url": url, "iri": iri}, "/task/cache_emoji") + @staticmethod + def send_webmentions(activity: ap.Create, links: Set[str]) -> None: + for link in links: + p.push( + { + "link": link, + "note_url": activity.get_object().get_url(), + "remote_id": activity.id, + }, + "/task/send_webmention", + ) + @staticmethod def cache_emojis(activity: ap.BaseActivity) -> None: for emoji in activity.get_emojis(): diff --git a/utils/lookup.py b/utils/lookup.py index 0f2ea2a..8739746 100644 --- a/utils/lookup.py +++ b/utils/lookup.py @@ -1,5 +1,3 @@ -import json - import little_boxes.activitypub as ap import mf2py import requests @@ -49,7 +47,7 @@ def lookup(url: str) -> ap.BaseActivity: # Maybe the page was JSON-LD? data = resp.json() return ap.parse_activity(data) - except json.JSONDecodeError: + except Exception: pass # Try content negotiation (retry with the AP Accept header) diff --git a/utils/webmentions.py b/utils/webmentions.py index 307ccc1..72fae7d 100644 --- a/utils/webmentions.py +++ b/utils/webmentions.py @@ -40,7 +40,7 @@ def _discover_webmention_endoint(url: str) -> Optional[str]: return None -def discover_webmention_endoint(url: str) -> Optional[str]: +def discover_webmention_endpoint(url: str) -> Optional[str]: """Discover the Webmention endpoint of a given URL, if any. Passes all the tests at https://webmention.rocks!