Support for sending webmentions as outgoing activities
parent
9417f38cc7
commit
c3c4475e24
|
@ -0,0 +1,28 @@
|
|||
"""Webmention support for outgoing activties
|
||||
|
||||
Revision ID: afc37d9c4fc0
|
||||
Revises: 65387f69edfb
|
||||
Create Date: 2022-07-10 14:20:46.311098
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'afc37d9c4fc0'
|
||||
down_revision = '65387f69edfb'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('outgoing_activity', sa.Column('webmention_target', sa.String(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('outgoing_activity', 'webmention_target')
|
||||
# ### end Alembic commands ###
|
|
@ -331,6 +331,9 @@ class OutgoingActivity(Base):
|
|||
inbox_object_id = Column(Integer, ForeignKey("inbox.id"), nullable=True)
|
||||
inbox_object = relationship(InboxObject, uselist=False)
|
||||
|
||||
# The source will be the outbox object URL
|
||||
webmention_target = Column(String, nullable=True)
|
||||
|
||||
tries = Column(Integer, nullable=False, default=0)
|
||||
next_try = Column(DateTime(timezone=True), nullable=True, default=now)
|
||||
|
||||
|
@ -422,6 +425,7 @@ class IndieAuthAccessToken(Base):
|
|||
id = Column(Integer, primary_key=True, index=True)
|
||||
created_at = Column(DateTime(timezone=True), nullable=False, default=now)
|
||||
|
||||
# Will be null for personal access tokens
|
||||
indieauth_authorization_request_id = Column(
|
||||
Integer, ForeignKey("indieauth_authorization_request.id"), nullable=True
|
||||
)
|
||||
|
|
|
@ -126,16 +126,20 @@ async def new_outgoing_activity(
|
|||
recipient: str,
|
||||
outbox_object_id: int | None,
|
||||
inbox_object_id: int | None = None,
|
||||
webmention_target: str | None = None,
|
||||
) -> models.OutgoingActivity:
|
||||
if outbox_object_id is None and inbox_object_id is None:
|
||||
raise ValueError("Must reference at least one inbox/outbox activity")
|
||||
elif outbox_object_id and inbox_object_id:
|
||||
if webmention_target and outbox_object_id is None:
|
||||
raise ValueError("Webmentions must reference an outbox activity")
|
||||
if outbox_object_id and inbox_object_id:
|
||||
raise ValueError("Cannot reference both inbox/outbox activities")
|
||||
|
||||
outgoing_activity = models.OutgoingActivity(
|
||||
recipient=recipient,
|
||||
outbox_object_id=outbox_object_id,
|
||||
inbox_object_id=inbox_object_id,
|
||||
webmention_target=webmention_target,
|
||||
)
|
||||
|
||||
db_session.add(outgoing_activity)
|
||||
|
@ -205,21 +209,39 @@ def process_next_outgoing_activity(db: Session) -> bool:
|
|||
next_activity.tries = next_activity.tries + 1
|
||||
next_activity.last_try = now()
|
||||
|
||||
payload = ap.wrap_object_if_needed(next_activity.anybox_object.ap_object)
|
||||
logger.info(f"recipient={next_activity.recipient}")
|
||||
|
||||
# Use LD sig if the activity may need to be forwarded by recipients
|
||||
if next_activity.anybox_object.is_from_outbox and payload["type"] in [
|
||||
"Create",
|
||||
"Update",
|
||||
"Delete",
|
||||
]:
|
||||
# But only if the object is public (to help with deniability/privacy)
|
||||
if next_activity.outbox_object.visibility == ap.VisibilityEnum.PUBLIC:
|
||||
ldsig.generate_signature(payload, k)
|
||||
|
||||
logger.info(f"{payload=}")
|
||||
try:
|
||||
resp = ap.post(next_activity.recipient, payload)
|
||||
if next_activity.webmention_target:
|
||||
webmention_payload = {
|
||||
"source": next_activity.outbox_object.url,
|
||||
"target": next_activity.webmention_target,
|
||||
}
|
||||
logger.info(f"{webmention_payload=}")
|
||||
resp = httpx.post(
|
||||
next_activity.recipient,
|
||||
data=webmention_payload,
|
||||
headers={
|
||||
"User-Agent": config.USER_AGENT,
|
||||
},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
else:
|
||||
payload = ap.wrap_object_if_needed(next_activity.anybox_object.ap_object)
|
||||
|
||||
# Use LD sig if the activity may need to be forwarded by recipients
|
||||
if next_activity.anybox_object.is_from_outbox and payload["type"] in [
|
||||
"Create",
|
||||
"Update",
|
||||
"Delete",
|
||||
]:
|
||||
# But only if the object is public (to help with deniability/privacy)
|
||||
if next_activity.outbox_object.visibility == ap.VisibilityEnum.PUBLIC:
|
||||
ldsig.generate_signature(payload, k)
|
||||
|
||||
logger.info(f"{payload=}")
|
||||
|
||||
resp = ap.post(next_activity.recipient, payload)
|
||||
except httpx.HTTPStatusError as http_error:
|
||||
logger.exception("Failed")
|
||||
next_activity.last_status_code = http_error.response.status_code
|
||||
|
|
|
@ -85,6 +85,37 @@ def test_process_next_outgoing_activity__server_200(
|
|||
recipient=recipient_inbox_url,
|
||||
outbox_object_id=outbox_object.id,
|
||||
inbox_object_id=None,
|
||||
webmention_target=None,
|
||||
)
|
||||
|
||||
# When processing the next outgoing activity
|
||||
# Then it is processed
|
||||
assert process_next_outgoing_activity(db) is True
|
||||
|
||||
assert respx_mock.calls.call_count == 1
|
||||
|
||||
outgoing_activity = db.query(models.OutgoingActivity).one()
|
||||
assert outgoing_activity.is_sent is True
|
||||
assert outgoing_activity.last_status_code == 204
|
||||
assert outgoing_activity.error is None
|
||||
assert outgoing_activity.is_errored is False
|
||||
|
||||
|
||||
def test_process_next_outgoing_activity__webmention(
|
||||
db: Session,
|
||||
respx_mock: respx.MockRouter,
|
||||
) -> None:
|
||||
# And an outgoing activity
|
||||
outbox_object = _setup_outbox_object()
|
||||
|
||||
recipient_url = "https://example.com/webmention"
|
||||
respx_mock.post(recipient_url).mock(return_value=httpx.Response(204))
|
||||
|
||||
outgoing_activity = factories.OutgoingActivityFactory(
|
||||
recipient=recipient_url,
|
||||
outbox_object_id=outbox_object.id,
|
||||
inbox_object_id=None,
|
||||
webmention_target="http://example.com",
|
||||
)
|
||||
|
||||
# When processing the next outgoing activity
|
||||
|
@ -114,6 +145,8 @@ def test_process_next_outgoing_activity__error_500(
|
|||
outgoing_activity = factories.OutgoingActivityFactory(
|
||||
recipient=recipient_inbox_url,
|
||||
outbox_object_id=outbox_object.id,
|
||||
inbox_object_id=None,
|
||||
webmention_target=None,
|
||||
)
|
||||
|
||||
# When processing the next outgoing activity
|
||||
|
@ -144,6 +177,8 @@ def test_process_next_outgoing_activity__errored(
|
|||
outgoing_activity = factories.OutgoingActivityFactory(
|
||||
recipient=recipient_inbox_url,
|
||||
outbox_object_id=outbox_object.id,
|
||||
inbox_object_id=None,
|
||||
webmention_target=None,
|
||||
tries=_MAX_RETRIES - 1,
|
||||
)
|
||||
|
||||
|
@ -176,6 +211,7 @@ def test_process_next_outgoing_activity__connect_error(
|
|||
recipient=recipient_inbox_url,
|
||||
outbox_object_id=outbox_object.id,
|
||||
inbox_object_id=None,
|
||||
webmention_target=None,
|
||||
)
|
||||
|
||||
# When processing the next outgoing activity
|
||||
|
|
Loading…
Reference in New Issue