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_id = Column(Integer, ForeignKey("inbox.id"), nullable=True)
|
||||||
inbox_object = relationship(InboxObject, uselist=False)
|
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)
|
tries = Column(Integer, nullable=False, default=0)
|
||||||
next_try = Column(DateTime(timezone=True), nullable=True, default=now)
|
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)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
created_at = Column(DateTime(timezone=True), nullable=False, default=now)
|
created_at = Column(DateTime(timezone=True), nullable=False, default=now)
|
||||||
|
|
||||||
|
# Will be null for personal access tokens
|
||||||
indieauth_authorization_request_id = Column(
|
indieauth_authorization_request_id = Column(
|
||||||
Integer, ForeignKey("indieauth_authorization_request.id"), nullable=True
|
Integer, ForeignKey("indieauth_authorization_request.id"), nullable=True
|
||||||
)
|
)
|
||||||
|
|
|
@ -126,16 +126,20 @@ async def new_outgoing_activity(
|
||||||
recipient: str,
|
recipient: str,
|
||||||
outbox_object_id: int | None,
|
outbox_object_id: int | None,
|
||||||
inbox_object_id: int | None = None,
|
inbox_object_id: int | None = None,
|
||||||
|
webmention_target: str | None = None,
|
||||||
) -> models.OutgoingActivity:
|
) -> models.OutgoingActivity:
|
||||||
if outbox_object_id is None and inbox_object_id is None:
|
if outbox_object_id is None and inbox_object_id is None:
|
||||||
raise ValueError("Must reference at least one inbox/outbox activity")
|
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")
|
raise ValueError("Cannot reference both inbox/outbox activities")
|
||||||
|
|
||||||
outgoing_activity = models.OutgoingActivity(
|
outgoing_activity = models.OutgoingActivity(
|
||||||
recipient=recipient,
|
recipient=recipient,
|
||||||
outbox_object_id=outbox_object_id,
|
outbox_object_id=outbox_object_id,
|
||||||
inbox_object_id=inbox_object_id,
|
inbox_object_id=inbox_object_id,
|
||||||
|
webmention_target=webmention_target,
|
||||||
)
|
)
|
||||||
|
|
||||||
db_session.add(outgoing_activity)
|
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.tries = next_activity.tries + 1
|
||||||
next_activity.last_try = now()
|
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:
|
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:
|
except httpx.HTTPStatusError as http_error:
|
||||||
logger.exception("Failed")
|
logger.exception("Failed")
|
||||||
next_activity.last_status_code = http_error.response.status_code
|
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,
|
recipient=recipient_inbox_url,
|
||||||
outbox_object_id=outbox_object.id,
|
outbox_object_id=outbox_object.id,
|
||||||
inbox_object_id=None,
|
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
|
# When processing the next outgoing activity
|
||||||
|
@ -114,6 +145,8 @@ def test_process_next_outgoing_activity__error_500(
|
||||||
outgoing_activity = factories.OutgoingActivityFactory(
|
outgoing_activity = factories.OutgoingActivityFactory(
|
||||||
recipient=recipient_inbox_url,
|
recipient=recipient_inbox_url,
|
||||||
outbox_object_id=outbox_object.id,
|
outbox_object_id=outbox_object.id,
|
||||||
|
inbox_object_id=None,
|
||||||
|
webmention_target=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# When processing the next outgoing activity
|
# When processing the next outgoing activity
|
||||||
|
@ -144,6 +177,8 @@ def test_process_next_outgoing_activity__errored(
|
||||||
outgoing_activity = factories.OutgoingActivityFactory(
|
outgoing_activity = factories.OutgoingActivityFactory(
|
||||||
recipient=recipient_inbox_url,
|
recipient=recipient_inbox_url,
|
||||||
outbox_object_id=outbox_object.id,
|
outbox_object_id=outbox_object.id,
|
||||||
|
inbox_object_id=None,
|
||||||
|
webmention_target=None,
|
||||||
tries=_MAX_RETRIES - 1,
|
tries=_MAX_RETRIES - 1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -176,6 +211,7 @@ def test_process_next_outgoing_activity__connect_error(
|
||||||
recipient=recipient_inbox_url,
|
recipient=recipient_inbox_url,
|
||||||
outbox_object_id=outbox_object.id,
|
outbox_object_id=outbox_object.id,
|
||||||
inbox_object_id=None,
|
inbox_object_id=None,
|
||||||
|
webmention_target=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# When processing the next outgoing activity
|
# When processing the next outgoing activity
|
||||||
|
|
Loading…
Reference in New Issue