kopia lustrzana https://github.com/snarfed/bridgy-fed
rodzic
647ae66763
commit
2150693893
19
common.py
19
common.py
|
@ -2,6 +2,7 @@
|
|||
"""Misc common utilities.
|
||||
"""
|
||||
from base64 import b64encode
|
||||
import copy
|
||||
import datetime
|
||||
from hashlib import sha256
|
||||
import itertools
|
||||
|
@ -36,7 +37,8 @@ LINK_HEADER_RE = re.compile(r""" *< *([^ >]+) *> *; *rel=['"]([^'"]+)['"] *""")
|
|||
#
|
||||
# ActivityPub Content-Type details:
|
||||
# https://www.w3.org/TR/activitypub/#retrieving-objects
|
||||
CONTENT_TYPE_AS2_LD = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
|
||||
CONTENT_TYPE_LD = 'application/ld+json'
|
||||
CONTENT_TYPE_AS2_LD = f'{CONTENT_TYPE_LD};profile="https://www.w3.org/ns/activitystreams"'
|
||||
CONTENT_TYPE_AS2 = 'application/activity+json'
|
||||
CONTENT_TYPE_AS1 = 'application/stream+json'
|
||||
CONTENT_TYPE_HTML = 'text/html; charset=utf-8'
|
||||
|
@ -46,9 +48,8 @@ CONTENT_TYPE_MAGIC_ENVELOPE = 'application/magic-envelope+xml'
|
|||
CONNEG_HEADERS_AS2 = {
|
||||
'Accept': '%s; q=0.9, %s; q=0.8' % (CONTENT_TYPE_AS2, CONTENT_TYPE_AS2_LD),
|
||||
}
|
||||
CONNEG_HEADERS_AS2_HTML = {
|
||||
'Accept': CONNEG_HEADERS_AS2['Accept'] + ', %s; q=0.7' % CONTENT_TYPE_HTML,
|
||||
}
|
||||
CONNEG_HEADERS_AS2_HTML = copy.deepcopy(CONNEG_HEADERS_AS2)
|
||||
CONNEG_HEADERS_AS2_HTML['Accept'] += ', {CONTENT_TYPE_HTML}; q=0.7'
|
||||
|
||||
SUPPORTED_VERBS = (
|
||||
'checkin',
|
||||
|
@ -189,7 +190,7 @@ def get_as2(url, user=None):
|
|||
raise err
|
||||
|
||||
resp = signed_get(url, user=user, headers=CONNEG_HEADERS_AS2_HTML)
|
||||
if content_type(resp) in (CONTENT_TYPE_AS2, CONTENT_TYPE_AS2_LD):
|
||||
if content_type(resp) in (CONTENT_TYPE_AS2, CONTENT_TYPE_LD):
|
||||
return resp
|
||||
|
||||
parsed = util.parse_html(resp)
|
||||
|
@ -200,7 +201,7 @@ def get_as2(url, user=None):
|
|||
|
||||
resp = signed_get(urllib.parse.urljoin(resp.url, as2['href']),
|
||||
headers=CONNEG_HEADERS_AS2)
|
||||
if content_type(resp) in (CONTENT_TYPE_AS2, CONTENT_TYPE_AS2_LD):
|
||||
if content_type(resp) in (CONTENT_TYPE_AS2, CONTENT_TYPE_LD):
|
||||
return resp
|
||||
|
||||
_error(resp)
|
||||
|
@ -310,7 +311,7 @@ def send_webmentions(activity_wrapped, proxy=None, **activity_props):
|
|||
return True
|
||||
|
||||
|
||||
def postprocess_as2(activity, user=None, target=None):
|
||||
def postprocess_as2(activity, user=None, target=None, create=True):
|
||||
"""Prepare an AS2 object to be served or sent via ActivityPub.
|
||||
|
||||
Args:
|
||||
|
@ -319,6 +320,8 @@ def postprocess_as2(activity, user=None, target=None):
|
|||
publicKey fields if needed.
|
||||
target: dict, AS2 object, optional. The target of activity's inReplyTo or
|
||||
Like/Announce/etc object, if any.
|
||||
create: boolean, whether to wrap `Note` and `Article` objects in a
|
||||
`Create` activity
|
||||
"""
|
||||
assert user
|
||||
type = activity.get('type')
|
||||
|
@ -420,7 +423,7 @@ def postprocess_as2(activity, user=None, target=None):
|
|||
to.append(as2.PUBLIC_AUDIENCE)
|
||||
|
||||
# wrap articles and notes in a Create activity
|
||||
if type in ('Article', 'Note'):
|
||||
if create and type in ('Article', 'Note'):
|
||||
activity = {
|
||||
'@context': as2.CONTEXT,
|
||||
'type': 'Create',
|
||||
|
|
58
redirect.py
58
redirect.py
|
@ -1,12 +1,14 @@
|
|||
"""Simple endpoint that redirects to the embedded fully qualified URL.
|
||||
"""Simple conneg endpoint that serves AS2 or redirects to to the original post.
|
||||
|
||||
May also instead fetch and convert to AS2, depending on conneg.
|
||||
|
||||
Used to wrap ActivityPub ids with the fed.brid.gy domain so that Mastodon
|
||||
accepts them. Background:
|
||||
Serves /r/https://foo.com/bar URL paths, where https://foo.com/bar is an
|
||||
original post. Needed for Mastodon interop, they require that AS2 object ids and
|
||||
urls are on the same domain that serves them. Background:
|
||||
|
||||
https://github.com/snarfed/bridgy-fed/issues/16#issuecomment-424799599
|
||||
https://github.com/tootsuite/mastodon/pull/6219#issuecomment-429142747
|
||||
|
||||
The conneg makes these /r/ URLs searchable in Mastodon:
|
||||
https://github.com/snarfed/bridgy-fed/issues/352
|
||||
"""
|
||||
import datetime
|
||||
import logging
|
||||
|
@ -16,19 +18,31 @@ import urllib.parse
|
|||
from flask import redirect, request
|
||||
from granary import as2, microformats2
|
||||
import mf2util
|
||||
from negotiator import ContentNegotiator, AcceptParameters, ContentType, Language
|
||||
from oauth_dropins.webutil import flask_util, util
|
||||
from oauth_dropins.webutil.flask_util import error
|
||||
from oauth_dropins.webutil.util import json_dumps
|
||||
from werkzeug.exceptions import abort
|
||||
|
||||
from app import app, cache
|
||||
import common
|
||||
from common import (
|
||||
CONTENT_TYPE_AS2,
|
||||
CONTENT_TYPE_AS2_LD,
|
||||
CONTENT_TYPE_HTML,
|
||||
postprocess_as2,
|
||||
)
|
||||
from models import User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CACHE_TIME = datetime.timedelta(seconds=15)
|
||||
|
||||
_negotiator = ContentNegotiator(acceptable=[
|
||||
AcceptParameters(ContentType(CONTENT_TYPE_HTML)),
|
||||
AcceptParameters(ContentType(CONTENT_TYPE_AS2)),
|
||||
AcceptParameters(ContentType(CONTENT_TYPE_AS2_LD)),
|
||||
])
|
||||
|
||||
|
||||
@app.get(r'/r/<path:to>')
|
||||
@flask_util.cached(cache, CACHE_TIME)
|
||||
|
@ -60,11 +74,17 @@ def redir(to):
|
|||
logger.info(f'No user found for any of {domains}; returning 404')
|
||||
abort(404)
|
||||
|
||||
# poor man's conneg, only handle single Accept values, not multiple with
|
||||
# priorities.
|
||||
if request.headers.get('Accept') in (common.CONTENT_TYPE_AS2,
|
||||
common.CONTENT_TYPE_AS2_LD):
|
||||
return convert_to_as2(to, user)
|
||||
# check conneg, serve AS2 if requested
|
||||
accept = request.headers.get('Accept')
|
||||
if accept:
|
||||
negotiated = _negotiator.negotiate(accept)
|
||||
if negotiated:
|
||||
type = str(negotiated.content_type)
|
||||
if type in (CONTENT_TYPE_AS2, CONTENT_TYPE_AS2_LD):
|
||||
return convert_to_as2(to, user), {
|
||||
'Content-Type': type,
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
}
|
||||
|
||||
# redirect
|
||||
logger.info(f'redirecting to {to}')
|
||||
|
@ -74,22 +94,18 @@ def redir(to):
|
|||
def convert_to_as2(url, domain):
|
||||
"""Fetch a URL as HTML, convert it to AS2, and return it.
|
||||
|
||||
Currently mainly for Pixelfed.
|
||||
https://github.com/snarfed/bridgy-fed/issues/39
|
||||
|
||||
Args:
|
||||
url: str
|
||||
domain: :class:`User`
|
||||
|
||||
Returns:
|
||||
dict: AS2 object
|
||||
"""
|
||||
mf2 = util.fetch_mf2(url)
|
||||
entry = mf2util.find_first_entry(mf2, ['h-entry'])
|
||||
logger.info(f"Parsed mf2 for {mf2['url']}: {json_dumps(entry, indent=2)}")
|
||||
|
||||
obj = common.postprocess_as2(as2.from_as1(microformats2.json_to_object(entry)),
|
||||
domain)
|
||||
obj = postprocess_as2(as2.from_as1(microformats2.json_to_object(entry)),
|
||||
domain, create=False)
|
||||
logger.info(f'Returning: {json_dumps(obj, indent=2)}')
|
||||
|
||||
return obj, {
|
||||
'Content-Type': common.CONTENT_TYPE_AS2,
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
}
|
||||
return obj
|
||||
|
|
|
@ -2,6 +2,7 @@ git+https://github.com/snarfed/django-salmon.git#egg=django_salmon
|
|||
git+https://github.com/snarfed/httpsig.git@HTTPSignatureAuth-sign-header#egg=httpsig
|
||||
git+https://github.com/snarfed/oauth-dropins.git#egg=oauth_dropins
|
||||
git+https://github.com/snarfed/granary.git#egg=granary
|
||||
git+https://github.com/snarfed/negotiator.git@py3#egg=negotiator
|
||||
git+https://github.com/dvska/gdata-python3.git#egg=gdata
|
||||
|
||||
beautifulsoup4==4.11.1
|
||||
|
|
Ładowanie…
Reference in New Issue