kopia lustrzana https://github.com/snarfed/bridgy-fed
rodzic
ba38d6853b
commit
212f2b11ec
|
@ -372,6 +372,10 @@ def postprocess_as2(activity, target=None):
|
|||
if not activity.get('id'):
|
||||
activity['id'] = util.get_first(activity, 'url')
|
||||
|
||||
# Deletes' object is our own id
|
||||
if type == 'Delete':
|
||||
activity['object'] = redirect_wrap(activity['object'])
|
||||
|
||||
# TODO: find a better way to check this, sometimes or always?
|
||||
# removed for now since it fires on posts without u-id or u-url, eg
|
||||
# https://chrisbeckstrom.com/2018/12/27/32551/
|
||||
|
|
|
@ -247,7 +247,7 @@ class User(StringIdModel):
|
|||
|
||||
# check home page
|
||||
try:
|
||||
obj = webmention.Webmention.load(self.homepage)
|
||||
obj = webmention.Webmention.load(self.homepage, gateway=True)
|
||||
self.actor_as2 = activitypub.postprocess_as2(as2.from_as1(obj.as1))
|
||||
self.has_hcard = True
|
||||
except (BadRequest, NotFound):
|
||||
|
|
1
pages.py
1
pages.py
|
@ -197,6 +197,7 @@ def fetch_objects(query):
|
|||
phrases = {
|
||||
'article': 'posted',
|
||||
'comment': 'replied',
|
||||
'delete': 'deleted',
|
||||
'follow': 'followed',
|
||||
'invite': 'is invited to',
|
||||
'issue': 'filed issue',
|
||||
|
|
|
@ -360,7 +360,7 @@ class Protocol:
|
|||
error(msg, status=int(errors[0][0] or 502))
|
||||
|
||||
@classmethod
|
||||
def load(cls, id, refresh=False):
|
||||
def load(cls, id, refresh=False, **kwargs):
|
||||
"""Loads and returns an Object from memory cache, datastore, or HTTP fetch.
|
||||
|
||||
Assumes id is a URL. Any fragment at the end is stripped before loading.
|
||||
|
@ -379,6 +379,7 @@ class Protocol:
|
|||
id: str
|
||||
refresh: boolean, whether to fetch the object remotely even if we have
|
||||
it stored
|
||||
kwargs: passed through to fetch()
|
||||
|
||||
Returns: :class:`Object`
|
||||
|
||||
|
@ -414,7 +415,7 @@ class Protocol:
|
|||
obj.new = True
|
||||
obj.changed = False
|
||||
|
||||
cls.fetch(obj)
|
||||
cls.fetch(obj, **kwargs)
|
||||
if orig_as1:
|
||||
obj.new = False
|
||||
obj.changed = as1.activity_changed(orig_as1, obj.as1)
|
||||
|
|
|
@ -50,6 +50,7 @@ Bridgy Fed takes some technical know-how to set up, and there are simpler (but l
|
|||
<li><a href="#image">How do I include an image in a post?</a></li>
|
||||
<li><a href="#hashtags">How do I use hashtags?</a></li>
|
||||
<li><a href="#update">How do I edit an existing post?</a></li>
|
||||
<li><a href="#delete">How do I delete a post?</a></li>
|
||||
<li><a href="#fragment">Can I publish just one part of a page?</a></li>
|
||||
<li><a href="#backfeed">How do fediverse replies, likes, and other interactions show up on my site?</a></li>
|
||||
<li><a href="#read">How do I read my fediverse timeline/feed?</a></li>
|
||||
|
@ -351,6 +352,12 @@ I love scotch. Scotchy scotchy scotch.
|
|||
</p>
|
||||
</li>
|
||||
|
||||
<li id="delete" class="question">How do I delete a post?</li>
|
||||
<li class="answer">
|
||||
<p>First, delete the post on your web site, so that HTTP requests for it return 410 Gone or 404 Not Found. Then, send another webmention to Bridgy Fed for it. Bridgy Fed will refetch the post, see that it's gone, and send an <a href="https://www.w3.org/TR/activitypub/#delete-activity-outbox"><code>Delete</code> activity</a> for it to the fediverse.
|
||||
</p>
|
||||
</li>
|
||||
|
||||
<li id="fragment" class="question">Can I publish just one part of a page?</li>
|
||||
<li class="answer">
|
||||
<p>If that HTML element has its own id, then sure! Just put the id in the fragment of the URL that you publish. For example, to publish the <code>bar</code> post here:</p>
|
||||
|
|
|
@ -6,7 +6,7 @@ from urllib.parse import urlencode
|
|||
|
||||
import feedparser
|
||||
from flask import g
|
||||
from granary import as2, atom, microformats2
|
||||
from granary import as1, as2, atom, microformats2
|
||||
from httpsig.sign import HeaderSigner
|
||||
from oauth_dropins.webutil import appengine_config, util
|
||||
from oauth_dropins.webutil.appengine_config import tasks_client
|
||||
|
@ -124,6 +124,21 @@ WEBMENTION_REL_LINK = requests_response(
|
|||
'<html><head><link rel="webmention" href="/webmention"></html>')
|
||||
WEBMENTION_NO_REL_LINK = requests_response('<html></html>')
|
||||
|
||||
DELETE_AS1 = {
|
||||
'objectType': 'activity',
|
||||
'verb': 'delete',
|
||||
'id': 'https://user.com/post#bridgy-fed-delete',
|
||||
'actor': 'http://localhost/user.com',
|
||||
'object': 'https://user.com/post',
|
||||
}
|
||||
DELETE_AS2 = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'type': 'Delete',
|
||||
'id': 'http://localhost/r/https://user.com/post#bridgy-fed-delete',
|
||||
'actor': 'http://localhost/user.com',
|
||||
'object': 'http://localhost/r/https://user.com/post',
|
||||
'to': [as2.PUBLIC_AUDIENCE],
|
||||
}
|
||||
|
||||
@mock.patch('requests.post')
|
||||
@mock.patch('requests.get')
|
||||
|
@ -354,7 +369,7 @@ class WebmentionTest(testutil.TestCase):
|
|||
|
||||
for inbox in inboxes:
|
||||
got = json_loads(calls[inbox][1]['data'])
|
||||
got.get('object', {}).pop('publicKey', None)
|
||||
as1.get_object(got).pop('publicKey', None)
|
||||
self.assert_equals(data, got, inbox)
|
||||
|
||||
def assert_object(self, id, **props):
|
||||
|
@ -1038,6 +1053,61 @@ class WebmentionTest(testutil.TestCase):
|
|||
self.req('https://user.com/follow'),
|
||||
))
|
||||
|
||||
def test_delete(self, mock_get, mock_post):
|
||||
mock_get.return_value = requests_response('"unused"', status=410,
|
||||
url='http://final/delete')
|
||||
mock_post.return_value = requests_response('unused', status=200)
|
||||
Object(id='https://user.com/post#bridgy-fed-create',
|
||||
mf2=self.note_mf2, status='complete').put()
|
||||
|
||||
self.make_followers()
|
||||
|
||||
got = self.client.post('/_ah/queue/webmention', data={
|
||||
'source': 'https://user.com/post',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
})
|
||||
self.assertEqual(200, got.status_code, got.text)
|
||||
|
||||
inboxes = ('https://inbox', 'https://public/inbox', 'https://shared/inbox')
|
||||
self.assert_deliveries(mock_post, inboxes, DELETE_AS2)
|
||||
|
||||
self.assert_object('https://user.com/post#bridgy-fed-delete',
|
||||
domains=['user.com'],
|
||||
source_protocol='webmention',
|
||||
status='complete',
|
||||
our_as1=DELETE_AS1,
|
||||
delivered=inboxes,
|
||||
type='delete',
|
||||
object_ids=['https://user.com/post'],
|
||||
labels=['user', 'activity'],
|
||||
)
|
||||
|
||||
def test_delete_no_object(self, mock_get, mock_post):
|
||||
mock_get.side_effect = [
|
||||
requests_response('"unused"', status=410, url='http://final/delete'),
|
||||
]
|
||||
got = self.client.post('/_ah/queue/webmention', data={
|
||||
'source': 'https://user.com/post',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
})
|
||||
self.assertEqual(304, got.status_code, got.text)
|
||||
mock_post.assert_not_called()
|
||||
|
||||
def test_delete_incomplete_response(self, mock_get, mock_post):
|
||||
mock_get.return_value = requests_response('"unused"', status=410,
|
||||
url='http://final/delete')
|
||||
|
||||
with app.test_request_context('/'):
|
||||
Object(id='https://user.com/post#bridgy-fed-create',
|
||||
mf2=self.note_mf2, status='in progress')
|
||||
|
||||
got = self.client.post('/_ah/queue/webmention', data={
|
||||
'source': 'https://user.com/post',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
})
|
||||
self.assertEqual(304, got.status_code, got.text)
|
||||
mock_post.assert_not_called()
|
||||
|
||||
def test_error(self, mock_get, mock_post):
|
||||
mock_get.side_effect = [self.follow, self.actor]
|
||||
mock_post.return_value = requests_response(
|
||||
|
@ -1204,7 +1274,7 @@ class WebmentionUtilTest(testutil.TestCase):
|
|||
def test_fetch_error(self, mock_get, __):
|
||||
mock_get.return_value = requests_response(REPOST_HTML, status=405)
|
||||
with self.assertRaises(BadGateway) as e:
|
||||
Webmention.fetch(Object(id='https://foo'))
|
||||
Webmention.fetch(Object(id='https://foo'), gateway=True)
|
||||
|
||||
def test_fetch_run_authorship(self, mock_get, __):
|
||||
mock_get.side_effect = [
|
||||
|
|
|
@ -15,7 +15,7 @@ from oauth_dropins.webutil.appengine_info import APP_ID
|
|||
from oauth_dropins.webutil.flask_util import error, flash
|
||||
from oauth_dropins.webutil.util import json_dumps, json_loads
|
||||
from oauth_dropins.webutil import webmention
|
||||
import requests
|
||||
from requests import HTTPError, RequestException, URLRequired
|
||||
from werkzeug.exceptions import BadGateway, BadRequest, HTTPException
|
||||
|
||||
# import module instead of individual classes/functions to avoid circular import
|
||||
|
@ -30,6 +30,8 @@ logger = logging.getLogger(__name__)
|
|||
# https://cloud.google.com/appengine/docs/locations
|
||||
TASKS_LOCATION = 'us-central1'
|
||||
|
||||
CHAR_AFTER_SPACE = chr(ord(' ') + 1)
|
||||
|
||||
|
||||
class Webmention(Protocol):
|
||||
"""Webmention protocol implementation."""
|
||||
|
@ -50,7 +52,7 @@ class Webmention(Protocol):
|
|||
return True
|
||||
|
||||
@classmethod
|
||||
def fetch(cls, obj):
|
||||
def fetch(cls, obj, gateway=False):
|
||||
"""Fetches a URL over HTTP and extracts its microformats2.
|
||||
|
||||
Follows redirects, but doesn't change the original URL in obj's id! The
|
||||
|
@ -59,15 +61,18 @@ class Webmention(Protocol):
|
|||
instead of the final redirect destination URL.
|
||||
|
||||
See :meth:`Protocol.fetch` for other background.
|
||||
|
||||
Args:
|
||||
gateway: passed through to :func:`webutil.util.fetch_mf2`
|
||||
"""
|
||||
url = obj.key.id()
|
||||
is_homepage = g.user and g.user.is_homepage(url)
|
||||
require_backlink = common.host_url().rstrip('/') if not is_homepage else None
|
||||
|
||||
try:
|
||||
parsed = util.fetch_mf2(url, gateway=True, require_backlink=require_backlink)
|
||||
except ValueError as e:
|
||||
logger.info(str(e))
|
||||
parsed = util.fetch_mf2(url, gateway=gateway,
|
||||
require_backlink=require_backlink)
|
||||
except (ValueError, URLRequired) as e:
|
||||
error(str(e))
|
||||
|
||||
if parsed is None:
|
||||
|
@ -184,13 +189,32 @@ def webmention_task():
|
|||
obj = Webmention.load(source, refresh=True)
|
||||
except BadRequest as e:
|
||||
error(str(e.description), status=304)
|
||||
except HTTPError as e:
|
||||
if e.response.status_code not in (410, 404):
|
||||
error(f'{e} ; {e.response.text if e.response else ""}', status=502)
|
||||
|
||||
# set actor to user
|
||||
props = obj.mf2['properties']
|
||||
author_urls = microformats2.get_string_urls(props.get('author', []))
|
||||
if author_urls and not g.user.is_homepage(author_urls[0]):
|
||||
logger.info(f'Overriding author {author_urls[0]} with {g.user.actor_id()}')
|
||||
props['author'] = [g.user.actor_id()]
|
||||
create_id = f'{source}#bridgy-fed-create'
|
||||
logger.info(f'Interpreting as Delete. Looking for {create_id}')
|
||||
create = models.Object.get_by_id(create_id)
|
||||
if not create or create.status != 'complete':
|
||||
error(f"Bridgy Fed hasn't successfully published {source}", status=304)
|
||||
|
||||
id = f'{source}#bridgy-fed-delete'
|
||||
obj = models.Object(id=id, our_as1={
|
||||
'id': id,
|
||||
'objectType': 'activity',
|
||||
'verb': 'delete',
|
||||
'actor': g.user.actor_id(),
|
||||
'object': source,
|
||||
})
|
||||
|
||||
if obj.mf2:
|
||||
# set actor to user
|
||||
props = obj.mf2['properties']
|
||||
author_urls = microformats2.get_string_urls(props.get('author', []))
|
||||
if author_urls and not g.user.is_homepage(author_urls[0]):
|
||||
logger.info(f'Overriding author {author_urls[0]} with {g.user.actor_id()}')
|
||||
props['author'] = [g.user.actor_id()]
|
||||
|
||||
logger.info(f'Converted to AS1: {obj.type}: {json_dumps(obj.as1, indent=2)}')
|
||||
|
||||
|
@ -334,7 +358,7 @@ def webmention_task():
|
|||
return last_success.text or 'Sent!', last_success.status_code
|
||||
elif isinstance(err, BadGateway):
|
||||
raise err
|
||||
elif isinstance(err, requests.HTTPError):
|
||||
elif isinstance(err, HTTPError):
|
||||
return str(err), err.status_code
|
||||
else:
|
||||
return str(err)
|
||||
|
@ -361,7 +385,7 @@ def _activitypub_targets(obj):
|
|||
domain = g.user.key.id()
|
||||
for follower in models.Follower.query().filter(
|
||||
models.Follower.key > Key('Follower', domain + ' '),
|
||||
models.Follower.key < Key('Follower', domain + chr(ord(' ') + 1))):
|
||||
models.Follower.key < Key('Follower', domain + CHAR_AFTER_SPACE)):
|
||||
if follower.status != 'inactive' and follower.last_follow:
|
||||
actor = follower.last_follow.get('actor')
|
||||
if actor and isinstance(actor, dict):
|
||||
|
@ -382,7 +406,7 @@ def _activitypub_targets(obj):
|
|||
# TODO: make this generic across protocols
|
||||
target_stored = activitypub.ActivityPub.load(target)
|
||||
target_obj = target_stored.as2 or as2.from_as1(target_stored.as1)
|
||||
except (requests.HTTPError, BadGateway) as e:
|
||||
except (HTTPError, BadGateway) as e:
|
||||
resp = getattr(e, 'requests_response', None)
|
||||
if resp and resp.ok:
|
||||
type = common.content_type(resp)
|
||||
|
|
Ładowanie…
Reference in New Issue