kopia lustrzana https://github.com/snarfed/bridgy-fed
docs: fix docstring formatting, other tweaks
rodzic
6442acb244
commit
db29ad7757
|
@ -67,7 +67,7 @@ class ActivityPub(User, Protocol):
|
||||||
"""Validate id, require URL, don't allow Bridgy Fed domains.
|
"""Validate id, require URL, don't allow Bridgy Fed domains.
|
||||||
|
|
||||||
TODO: normalize scheme and domain to lower case. Add that to
|
TODO: normalize scheme and domain to lower case. Add that to
|
||||||
:class:`util.UrlCanonicalizer`?
|
:class:`oauth_dropins.webutil.util.UrlCanonicalizer`\?
|
||||||
"""
|
"""
|
||||||
super()._pre_put_hook()
|
super()._pre_put_hook()
|
||||||
id = self.key.id()
|
id = self.key.id()
|
||||||
|
@ -149,7 +149,7 @@ class ActivityPub(User, Protocol):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def target_for(cls, obj, shared=False):
|
def target_for(cls, obj, shared=False):
|
||||||
"""Returns `obj`'s or its author's/actor's inbox, if available."""
|
"""Returns ``obj``'s or its author's/actor's inbox, if available."""
|
||||||
# TODO: we have entities in prod that fail this, eg
|
# TODO: we have entities in prod that fail this, eg
|
||||||
# https://indieweb.social/users/bismark has source_protocol webmention
|
# https://indieweb.social/users/bismark has source_protocol webmention
|
||||||
# assert obj.source_protocol in (cls.LABEL, cls.ABBREV, 'ui', None), str(obj)
|
# assert obj.source_protocol in (cls.LABEL, cls.ABBREV, 'ui', None), str(obj)
|
||||||
|
@ -189,8 +189,8 @@ class ActivityPub(User, Protocol):
|
||||||
def send(to_cls, obj, url, log_data=True):
|
def send(to_cls, obj, url, log_data=True):
|
||||||
"""Delivers an activity to an inbox URL.
|
"""Delivers an activity to an inbox URL.
|
||||||
|
|
||||||
If `obj.recipient_obj` is set, it's interpreted as the receiving actor
|
If ``obj.recipient_obj`` is set, it's interpreted as the receiving actor
|
||||||
who we're delivering to and its id is populated into `cc`.
|
who we're delivering to and its id is populated into ``cc``.
|
||||||
"""
|
"""
|
||||||
if to_cls.is_blocklisted(url):
|
if to_cls.is_blocklisted(url):
|
||||||
logger.info(f'Skipping sending to {url}')
|
logger.info(f'Skipping sending to {url}')
|
||||||
|
@ -213,47 +213,48 @@ class ActivityPub(User, Protocol):
|
||||||
def fetch(cls, obj, **kwargs):
|
def fetch(cls, obj, **kwargs):
|
||||||
"""Tries to fetch an AS2 object.
|
"""Tries to fetch an AS2 object.
|
||||||
|
|
||||||
Assumes obj.id is a URL. Any fragment at the end is stripped before
|
Assumes ``obj.id`` is a URL. Any fragment at the end is stripped before
|
||||||
loading. This is currently underspecified and somewhat inconsistent
|
loading. This is currently underspecified and somewhat inconsistent
|
||||||
across AP implementations:
|
across AP implementations:
|
||||||
|
|
||||||
https://socialhub.activitypub.rocks/t/problems-posting-to-mastodon-inbox/801/11
|
* https://socialhub.activitypub.rocks/t/problems-posting-to-mastodon-inbox/801/11
|
||||||
https://socialhub.activitypub.rocks/t/problems-posting-to-mastodon-inbox/801/23
|
* https://socialhub.activitypub.rocks/t/problems-posting-to-mastodon-inbox/801/23
|
||||||
https://socialhub.activitypub.rocks/t/s2s-create-activity/1647/5
|
* https://socialhub.activitypub.rocks/t/s2s-create-activity/1647/5
|
||||||
https://github.com/mastodon/mastodon/issues/13879 (open!)
|
* https://github.com/mastodon/mastodon/issues/13879 (open!)
|
||||||
https://github.com/w3c/activitypub/issues/224
|
* https://github.com/w3c/activitypub/issues/224
|
||||||
|
|
||||||
Uses HTTP content negotiation via the Content-Type header. If the url is
|
Uses HTTP content negotiation via the ``Content-Type`` header. If the
|
||||||
HTML and it has a rel-alternate link with an AS2 content type, fetches and
|
url is HTML and it has a ``rel-alternate`` link with an AS2 content
|
||||||
returns that URL.
|
type, fetches and returns that URL.
|
||||||
|
|
||||||
Includes an HTTP Signature with the request.
|
Includes an HTTP Signature with the request.
|
||||||
https://w3c.github.io/activitypub/#authorization
|
|
||||||
https://tools.ietf.org/html/draft-cavage-http-signatures-07
|
|
||||||
https://github.com/mastodon/mastodon/pull/11269
|
|
||||||
|
|
||||||
Mastodon requires this signature if AUTHORIZED_FETCH aka secure mode is on:
|
* https://w3c.github.io/activitypub/#authorization
|
||||||
https://docs.joinmastodon.org/admin/config/#authorized_fetch
|
* https://tools.ietf.org/html/draft-cavage-http-signatures-07
|
||||||
|
* https://github.com/mastodon/mastodon/pull/11269
|
||||||
|
|
||||||
|
Mastodon requires this signature if ``AUTHORIZED_FETCH`` aka secure mode
|
||||||
|
is on: https://docs.joinmastodon.org/admin/config/#authorized_fetch
|
||||||
|
|
||||||
Signs the request with the current user's key. If not provided, defaults to
|
Signs the request with the current user's key. If not provided, defaults to
|
||||||
using @snarfed.org@snarfed.org's key.
|
using @snarfed.org@snarfed.org's key.
|
||||||
|
|
||||||
See :meth:`Protocol.fetch` for more details.
|
See :meth:`protocol.Protocol.fetch` for more details.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj: :class:`Object` with the id to fetch. Fills data into the as2
|
obj (models.Object): with the id to fetch. Fills data into the as2
|
||||||
property.
|
property.
|
||||||
kwargs: ignored
|
kwargs: ignored
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if the object was fetched and populated successfully,
|
bool: True if the object was fetched and populated successfully,
|
||||||
False otherwise
|
False otherwise
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:class:`requests.HTTPError`, :class:`werkzeug.exceptions.HTTPException`
|
requests.HTTPError:
|
||||||
|
werkzeug.exceptions.HTTPException: will have an additional
|
||||||
If we raise a werkzeug HTTPException, it will have an additional
|
``requests_response`` attribute with the last
|
||||||
requests_response attribute with the last requests.Response we received.
|
:class:`requests.Response` we received.
|
||||||
"""
|
"""
|
||||||
url = obj.key.id()
|
url = obj.key.id()
|
||||||
if not util.is_web(url):
|
if not util.is_web(url):
|
||||||
|
@ -329,7 +330,7 @@ class ActivityPub(User, Protocol):
|
||||||
"""Verifies the current request's HTTP Signature.
|
"""Verifies the current request's HTTP Signature.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
activity: dict, AS2 activity
|
activity (dict): AS2 activity
|
||||||
|
|
||||||
Logs details of the result. Raises :class:`werkzeug.HTTPError` if the
|
Logs details of the result. Raises :class:`werkzeug.HTTPError` if the
|
||||||
signature is missing or invalid, otherwise does nothing and returns None.
|
signature is missing or invalid, otherwise does nothing and returns None.
|
||||||
|
@ -417,19 +418,20 @@ def signed_post(url, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
def signed_request(fn, url, data=None, log_data=True, headers=None, **kwargs):
|
def signed_request(fn, url, data=None, log_data=True, headers=None, **kwargs):
|
||||||
"""Wraps requests.* and adds HTTP Signature.
|
"""Wraps ``requests.*`` and adds HTTP Signature.
|
||||||
|
|
||||||
If the current session has a user (ie in g.user), signs with that user's
|
If the current session has a user (ie in ``g.user``), signs with that user's
|
||||||
key. Otherwise, uses the default user snarfed.org.
|
key. Otherwise, uses the default user snarfed.org.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
fn: :func:`util.requests_get` or :func:`util.requests_get`
|
fn (callable): :func:`util.requests_get` or :func:`util.requests_get`
|
||||||
url: str
|
url (str):
|
||||||
data: optional AS2 object
|
data (dict): optional AS2 object
|
||||||
log_data: boolean, whether to log full data object
|
log_data (bool): whether to log full data object
|
||||||
kwargs: passed through to requests
|
kwargs: passed through to requests
|
||||||
|
|
||||||
Returns: :class:`requests.Response`
|
Returns:
|
||||||
|
requests.Response:
|
||||||
"""
|
"""
|
||||||
if headers is None:
|
if headers is None:
|
||||||
headers = {}
|
headers = {}
|
||||||
|
@ -490,13 +492,15 @@ def signed_request(fn, url, data=None, log_data=True, headers=None, **kwargs):
|
||||||
def postprocess_as2(activity, orig_obj=None, wrap=True):
|
def postprocess_as2(activity, orig_obj=None, wrap=True):
|
||||||
"""Prepare an AS2 object to be served or sent via ActivityPub.
|
"""Prepare an AS2 object to be served or sent via ActivityPub.
|
||||||
|
|
||||||
g.user is required. Populates it into the actor.id and publicKey fields.
|
``g.user`` is required. Populates it into the ``actor.id`` and ``publicKey``
|
||||||
|
fields.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
activity: dict, AS2 object or activity
|
activity (dict): AS2 object or activity
|
||||||
orig_obj: dict, AS2 object, optional. The target of activity's inReplyTo or
|
orig_obj (dict): AS2 object, optional. The target of activity's inReplyTo or
|
||||||
Like/Announce/etc object, if any.
|
Like/Announce/etc object, if any.
|
||||||
wrap: boolean, whether to wrap id, url, object, actor, and attributedTo
|
wrap (bool): whether to wrap id, url, object, actor, and attributedTo
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not activity or isinstance(activity, str):
|
if not activity or isinstance(activity, str):
|
||||||
return activity
|
return activity
|
||||||
|
@ -650,8 +654,8 @@ def postprocess_as2_actor(actor, wrap=True):
|
||||||
Modifies actor in place.
|
Modifies actor in place.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
actor: dict, AS2 actor object
|
actor (dict): AS2 actor object
|
||||||
wrap: boolean, whether to wrap url
|
wrap (bool): whether to wrap url
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
actor dict
|
actor dict
|
||||||
|
|
25
atproto.py
25
atproto.py
|
@ -122,10 +122,10 @@ class ATProto(User, Protocol):
|
||||||
returning Bridgy Fed's URL as the PDS.
|
returning Bridgy Fed's URL as the PDS.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj: :class:`Object`
|
obj (Object)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str
|
str:
|
||||||
"""
|
"""
|
||||||
id = obj.key.id()
|
id = obj.key.id()
|
||||||
if id.startswith('did:'):
|
if id.startswith('did:'):
|
||||||
|
@ -169,10 +169,10 @@ class ATProto(User, Protocol):
|
||||||
def _pds_for(cls, did_obj):
|
def _pds_for(cls, did_obj):
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
did_obj: :class:`Object`
|
did_obj (Object)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str, PDS URL, or None
|
str: PDS URL, or None
|
||||||
"""
|
"""
|
||||||
assert did_obj.key.id().startswith('did:')
|
assert did_obj.key.id().startswith('did:')
|
||||||
|
|
||||||
|
@ -195,7 +195,7 @@ class ATProto(User, Protocol):
|
||||||
"""Creates an ATProto user, repo, and profile for a non-ATProto user.
|
"""Creates an ATProto user, repo, and profile for a non-ATProto user.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user (User)
|
user (models.User)
|
||||||
"""
|
"""
|
||||||
assert not isinstance(user, ATProto)
|
assert not isinstance(user, ATProto)
|
||||||
|
|
||||||
|
@ -321,12 +321,12 @@ class ATProto(User, Protocol):
|
||||||
"""Tries to fetch a ATProto object.
|
"""Tries to fetch a ATProto object.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj: :class:`Object` with the id to fetch. Fills data into the as2
|
obj (models.Object): with the id to fetch. Fills data into the ``as2``
|
||||||
property.
|
property.
|
||||||
kwargs: ignored
|
kwargs: ignored
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if the object was fetched and populated successfully,
|
bool: True if the object was fetched and populated successfully,
|
||||||
False otherwise
|
False otherwise
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
|
@ -364,12 +364,13 @@ class ATProto(User, Protocol):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def serve(cls, obj):
|
def serve(cls, obj):
|
||||||
"""Serves an :class:`Object` as AS2.
|
"""Serves an :class:`models.Object` as AS2.
|
||||||
|
|
||||||
This is minimally implemented to serve app.bsky.* lexicon data, but
|
This is minimally implemented to serve ``app.bsky.*`` lexicon data, but
|
||||||
BGSes and other clients will generally receive ATProto commits via
|
BGSes and other clients will generally receive ATProto commits via
|
||||||
`com.atproto.sync.subscribeRepos` subscriptions, not BF-specific
|
``com.atproto.sync.subscribeRepos`` subscriptions, not BF-specific
|
||||||
/convert/... HTTP requests, so this should never be used in practice.
|
``/convert/...`` HTTP requests, so this should never be used in
|
||||||
|
practice.
|
||||||
"""
|
"""
|
||||||
return bluesky.from_as1(obj.as1), {'Content-Type': 'application/json'}
|
return bluesky.from_as1(obj.as1), {'Content-Type': 'application/json'}
|
||||||
|
|
||||||
|
@ -378,7 +379,7 @@ class ATProto(User, Protocol):
|
||||||
def poll_notifications():
|
def poll_notifications():
|
||||||
"""Fetches and enqueueus new activities from the AppView for our users.
|
"""Fetches and enqueueus new activities from the AppView for our users.
|
||||||
|
|
||||||
Uses the `listNotifications` endpoint, which is intended for end users. 🤷
|
Uses the ``listNotifications`` endpoint, which is intended for end users. 🤷
|
||||||
|
|
||||||
https://github.com/bluesky-social/atproto/discussions/1538
|
https://github.com/bluesky-social/atproto/discussions/1538
|
||||||
"""
|
"""
|
||||||
|
|
55
common.py
55
common.py
|
@ -1,6 +1,4 @@
|
||||||
# coding=utf-8
|
"""Misc common utilities."""
|
||||||
"""Misc common utilities.
|
|
||||||
"""
|
|
||||||
import base64
|
import base64
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
|
@ -75,18 +73,18 @@ TASKS_LOCATION = 'us-central1'
|
||||||
|
|
||||||
|
|
||||||
def base64_to_long(x):
|
def base64_to_long(x):
|
||||||
"""Converts x from URL safe base64 encoding to a long integer.
|
"""Converts from URL safe base64 encoding to long integer.
|
||||||
|
|
||||||
Originally from django_salmon.magicsigs. Used in :meth:`User.public_pem`
|
Originally from ``django_salmon.magicsigs``. Used in :meth:`User.public_pem`
|
||||||
and :meth:`User.private_pem`.
|
and :meth:`User.private_pem`.
|
||||||
"""
|
"""
|
||||||
return number.bytes_to_long(base64.urlsafe_b64decode(x))
|
return number.bytes_to_long(base64.urlsafe_b64decode(x))
|
||||||
|
|
||||||
|
|
||||||
def long_to_base64(x):
|
def long_to_base64(x):
|
||||||
"""Converts x from a long integer to base64 URL safe encoding.
|
"""Converts from long integer to base64 URL safe encoding.
|
||||||
|
|
||||||
Originally from django_salmon.magicsigs. Used in :meth:`User.get_or_create`.
|
Originally from ``django_salmon.magicsigs``. Used in :meth:`User.get_or_create`.
|
||||||
"""
|
"""
|
||||||
return base64.urlsafe_b64encode(number.long_to_bytes(x))
|
return base64.urlsafe_b64encode(number.long_to_bytes(x))
|
||||||
|
|
||||||
|
@ -103,22 +101,22 @@ def host_url(path_query=None):
|
||||||
|
|
||||||
|
|
||||||
def error(msg, status=400, exc_info=None, **kwargs):
|
def error(msg, status=400, exc_info=None, **kwargs):
|
||||||
"""Like flask_util.error, but wraps body in JSON."""
|
"""Like :func:`oauth_dropins.webutil.flask_util.error`, but wraps body in JSON."""
|
||||||
logger.info(f'Returning {status}: {msg}', exc_info=exc_info)
|
logger.info(f'Returning {status}: {msg}', exc_info=exc_info)
|
||||||
abort(status, response=make_response({'error': msg}, status), **kwargs)
|
abort(status, response=make_response({'error': msg}, status), **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def pretty_link(url, text=None, **kwargs):
|
def pretty_link(url, text=None, **kwargs):
|
||||||
"""Wrapper around util.pretty_link() that converts Mastodon user URLs to @-@.
|
"""Wrapper around :func:`oauth_dropins.webutil.util.pretty_link` that converts Mastodon user URLs to @-@ handles.
|
||||||
|
|
||||||
Eg for URLs like https://mastodon.social/@foo and
|
Eg for URLs like https://mastodon.social/@foo and
|
||||||
https://mastodon.social/users/foo, defaults text to @foo@mastodon.social if
|
https://mastodon.social/users/foo, defaults text to @foo@mastodon.social if
|
||||||
it's not provided.
|
it's not provided.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
url: str
|
url (str)
|
||||||
text: str
|
text (str)
|
||||||
kwargs: passed through to :func:`webutil.util.pretty_link`
|
kwargs: passed through to :func:`oauth_dropins.webutil.util.pretty_link`
|
||||||
"""
|
"""
|
||||||
if g.user and g.user.is_web_url(url):
|
if g.user and g.user.is_web_url(url):
|
||||||
return g.user.user_page_link()
|
return g.user.user_page_link()
|
||||||
|
@ -144,12 +142,13 @@ def redirect_wrap(url):
|
||||||
...to satisfy Mastodon's non-standard domain matching requirement. :(
|
...to satisfy Mastodon's non-standard domain matching requirement. :(
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
url: string
|
url (str)
|
||||||
|
|
||||||
* https://github.com/snarfed/bridgy-fed/issues/16#issuecomment-424799599
|
* https://github.com/snarfed/bridgy-fed/issues/16#issuecomment-424799599
|
||||||
* https://github.com/tootsuite/mastodon/pull/6219#issuecomment-429142747
|
* https://github.com/tootsuite/mastodon/pull/6219#issuecomment-429142747
|
||||||
|
|
||||||
Returns: string, redirect url
|
Returns:
|
||||||
|
str: redirect url
|
||||||
"""
|
"""
|
||||||
if not url or util.domain_from_link(url) in DOMAINS:
|
if not url or util.domain_from_link(url) in DOMAINS:
|
||||||
return url
|
return url
|
||||||
|
@ -160,15 +159,16 @@ def redirect_wrap(url):
|
||||||
def redirect_unwrap(val):
|
def redirect_unwrap(val):
|
||||||
"""Removes our redirect wrapping from a URL, if it's there.
|
"""Removes our redirect wrapping from a URL, if it's there.
|
||||||
|
|
||||||
val may be a string, dict, or list. dicts and lists are unwrapped
|
``val`` may be a string, dict, or list. dicts and lists are unwrapped
|
||||||
recursively.
|
recursively.
|
||||||
|
|
||||||
Strings that aren't wrapped URLs are left unchanged.
|
Strings that aren't wrapped URLs are left unchanged.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
val: string or dict or list
|
val (str or dict or list)
|
||||||
|
|
||||||
Returns: string, unwrapped url
|
Returns:
|
||||||
|
str: unwrapped url
|
||||||
"""
|
"""
|
||||||
if isinstance(val, dict):
|
if isinstance(val, dict):
|
||||||
return {k: redirect_unwrap(v) for k, v in val.items()}
|
return {k: redirect_unwrap(v) for k, v in val.items()}
|
||||||
|
@ -196,15 +196,16 @@ def redirect_unwrap(val):
|
||||||
def webmention_endpoint_cache_key(url):
|
def webmention_endpoint_cache_key(url):
|
||||||
"""Returns cache key for a cached webmention endpoint for a given URL.
|
"""Returns cache key for a cached webmention endpoint for a given URL.
|
||||||
|
|
||||||
Just the domain by default. If the URL is the home page, ie path is / , the
|
Just the domain by default. If the URL is the home page, ie path is ``/``,
|
||||||
key includes a / at the end, so that we cache webmention endpoints for home
|
the key includes a ``/`` at the end, so that we cache webmention endpoints
|
||||||
pages separate from other pages. https://github.com/snarfed/bridgy/issues/701
|
for home pages separate from other pages.
|
||||||
|
https://github.com/snarfed/bridgy/issues/701
|
||||||
|
|
||||||
Example: 'snarfed.org /'
|
Example: ``snarfed.org /``
|
||||||
|
|
||||||
https://github.com/snarfed/bridgy-fed/issues/423
|
https://github.com/snarfed/bridgy-fed/issues/423
|
||||||
|
|
||||||
Adapted from bridgy/util.py.
|
Adapted from ``bridgy/util.py``.
|
||||||
"""
|
"""
|
||||||
parsed = urllib.parse.urlparse(url)
|
parsed = urllib.parse.urlparse(url)
|
||||||
key = parsed.netloc
|
key = parsed.netloc
|
||||||
|
@ -225,7 +226,7 @@ def webmention_discover(url, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
def add(seq, val):
|
def add(seq, val):
|
||||||
"""Appends val to seq if seq doesn't already contain it.
|
"""Appends ``val`` to ``seq`` if seq doesn't already contain it.
|
||||||
|
|
||||||
Useful for treating repeated ndb properties like sets instead of lists.
|
Useful for treating repeated ndb properties like sets instead of lists.
|
||||||
"""
|
"""
|
||||||
|
@ -240,13 +241,13 @@ def create_task(queue, **params):
|
||||||
creating a task.
|
creating a task.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
queue: string, queue name
|
queue (str): queue name
|
||||||
params: form-encoded and included in the task request body
|
params: form-encoded and included in the task request body
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
:flask:`Response` from running the task inline if running in a local
|
flask.Response or (str, int): response from either running the task
|
||||||
server, otherwise (str response body, int status code) response from
|
inline, if running in a local server, or the response from creating the
|
||||||
creating the task.
|
task.
|
||||||
"""
|
"""
|
||||||
assert queue
|
assert queue
|
||||||
path = f'/queue/{queue}'
|
path = f'/queue/{queue}'
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"""Serves /convert/... URLs to convert data from one protocol to another.
|
"""Serves ``/convert/...`` URLs to convert data from one protocol to another.
|
||||||
|
|
||||||
URL pattern is /convert/SOURCE/DEST , where SOURCE and DEST are the LABEL
|
URL pattern is ``/convert/SOURCE/DEST``, where ``SOURCE`` and ``DEST`` are the
|
||||||
constants from the :class:`Protocol` subclasses.
|
``LABEL`` constants from the :class:`protocol.Protocol` subclasses.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
|
@ -28,4 +28,8 @@ source ../local/bin/activate
|
||||||
|
|
||||||
# Run sphinx in the virtualenv's python interpreter so it can import packages
|
# Run sphinx in the virtualenv's python interpreter so it can import packages
|
||||||
# installed in the virtualenv.
|
# installed in the virtualenv.
|
||||||
|
#
|
||||||
|
# If sphinx crashes with eg:
|
||||||
|
# exception: '<' not supported between instances of 'dict' and 'dict'
|
||||||
|
# ...try running with -E to clear its cache.
|
||||||
python3 `which sphinx-build` -b html . _build/html
|
python3 `which sphinx-build` -b html . _build/html
|
||||||
|
|
17
docs/conf.py
17
docs/conf.py
|
@ -342,17 +342,18 @@ texinfo_documents = [
|
||||||
|
|
||||||
# Example configuration for intersphinx: refer to the Python standard library.
|
# Example configuration for intersphinx: refer to the Python standard library.
|
||||||
intersphinx_mapping = {
|
intersphinx_mapping = {
|
||||||
'arroba': ('https://arroba.readthedocs.io/en/latest', None),
|
'arroba': ('https://arroba.readthedocs.io/en/stable', None),
|
||||||
'dag_cbor': ('https://dag-cbor.readthedocs.io/en/latest', None),
|
'dag_cbor': ('https://dag-cbor.readthedocs.io/en/stable', None),
|
||||||
'flask': ('https://flask.palletsprojects.com/en/latest', None),
|
'flask': ('https://flask.palletsprojects.com/en/latest', None),
|
||||||
'flask_caching': ('https://flask-caching.readthedocs.io/en/latest', None),
|
'flask_caching': ('https://flask-caching.readthedocs.io/en/latest', None),
|
||||||
'granary': ('https://granary.readthedocs.io/en/latest', None),
|
'granary': ('https://granary.readthedocs.io/en/stable', None),
|
||||||
'multiformats': ('https://multiformats.readthedocs.io/en/latest', None),
|
'lexrpc': ('https://granary.readthedocs.io/en/stable', None),
|
||||||
'oauth_dropins': ('https://oauth-dropins.readthedocs.io/en/latest', None),
|
'multiformats': ('https://multiformats.readthedocs.io/en/stable', None),
|
||||||
|
'oauth_dropins': ('https://oauth-dropins.readthedocs.io/en/stable', None),
|
||||||
'python': ('https://docs.python.org/3/', None),
|
'python': ('https://docs.python.org/3/', None),
|
||||||
'requests': ('https://requests.readthedocs.io/en/stable/', None),
|
'requests': ('https://requests.readthedocs.io/en/stable', None),
|
||||||
'urllib3': ('https://urllib3.readthedocs.io/en/latest', None),
|
'urllib3': ('https://urllib3.readthedocs.io/en/stable', None),
|
||||||
'werkzeug': ('https://werkzeug.palletsprojects.com/en/latest/', None),
|
'werkzeug': ('https://werkzeug.palletsprojects.com/en/latest', None),
|
||||||
}
|
}
|
||||||
|
|
||||||
# -- Post process ------------------------------------------------------------
|
# -- Post process ------------------------------------------------------------
|
||||||
|
|
|
@ -8,51 +8,64 @@ Reference documentation.
|
||||||
activitypub
|
activitypub
|
||||||
-----------
|
-----------
|
||||||
.. automodule:: activitypub
|
.. automodule:: activitypub
|
||||||
|
:exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__
|
||||||
|
|
||||||
atproto
|
atproto
|
||||||
-------
|
-------
|
||||||
.. automodule:: atproto
|
.. automodule:: atproto
|
||||||
|
:exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__
|
||||||
|
|
||||||
common
|
common
|
||||||
------
|
------
|
||||||
.. automodule:: common
|
.. automodule:: common
|
||||||
|
:exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__
|
||||||
|
|
||||||
convert
|
convert
|
||||||
-------
|
-------
|
||||||
.. automodule:: convert
|
.. automodule:: convert
|
||||||
|
:exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__
|
||||||
|
|
||||||
follow
|
follow
|
||||||
------
|
------
|
||||||
.. automodule:: follow
|
.. automodule:: follow
|
||||||
|
:exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__
|
||||||
|
|
||||||
models
|
models
|
||||||
------
|
------
|
||||||
.. automodule:: models
|
.. automodule:: models
|
||||||
|
:exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__
|
||||||
|
|
||||||
pages
|
pages
|
||||||
-----
|
-----
|
||||||
.. automodule:: pages
|
.. automodule:: pages
|
||||||
|
:exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__
|
||||||
|
|
||||||
protocol
|
protocol
|
||||||
--------
|
--------
|
||||||
.. automodule:: protocol
|
.. automodule:: protocol
|
||||||
|
:exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__
|
||||||
|
|
||||||
redirect
|
redirect
|
||||||
--------
|
--------
|
||||||
.. automodule:: redirect
|
.. automodule:: redirect
|
||||||
|
:exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__
|
||||||
|
|
||||||
render
|
render
|
||||||
------
|
------
|
||||||
.. automodule:: render
|
.. automodule:: render
|
||||||
|
:exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__
|
||||||
|
|
||||||
superfeedr
|
superfeedr
|
||||||
----------
|
----------
|
||||||
.. automodule:: superfeedr
|
.. automodule:: superfeedr
|
||||||
|
:exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__
|
||||||
|
|
||||||
web
|
web
|
||||||
---
|
---
|
||||||
.. automodule:: web
|
.. automodule:: web
|
||||||
|
:exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__
|
||||||
|
|
||||||
webfinger
|
webfinger
|
||||||
---------
|
---------
|
||||||
.. automodule:: webfinger
|
.. automodule:: webfinger
|
||||||
|
:exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
"""Remote follow handler.
|
"""Remote follow handler.
|
||||||
|
|
||||||
https://github.com/snarfed/bridgy-fed/issues/60
|
* https://github.com/snarfed/bridgy-fed/issues/60
|
||||||
https://socialhub.activitypub.rocks/t/what-is-the-current-spec-for-remote-follow/2020
|
* https://socialhub.activitypub.rocks/t/what-is-the-current-spec-for-remote-follow/2020
|
||||||
https://www.rfc-editor.org/rfc/rfc7033
|
* https://www.rfc-editor.org/rfc/rfc7033
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
8
ids.py
8
ids.py
|
@ -12,8 +12,8 @@ def convert_id(*, id, from_proto, to_proto):
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
id (str)
|
id (str)
|
||||||
from_proto (Protocol)
|
from_proto (protocol.Protocol)
|
||||||
to_proto (Protocol)
|
to_proto (protocol.Protocol)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: the corresponding id in ``to_proto``
|
str: the corresponding id in ``to_proto``
|
||||||
|
@ -49,8 +49,8 @@ def convert_handle(*, handle, from_proto, to_proto):
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
handle (str)
|
handle (str)
|
||||||
from_proto (Protocol)
|
from_proto (protocol.Protocol)
|
||||||
to_proto (Protocol)
|
to_proto (protocol.Protocol)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: the corresponding handle in ``to_proto``
|
str: the corresponding handle in ``to_proto``
|
||||||
|
|
145
models.py
145
models.py
|
@ -51,20 +51,22 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Target(ndb.Model):
|
class Target(ndb.Model):
|
||||||
"""Protocol + URI pairs for identifying objects.
|
""":class:`protocol.Protocol` + URI pairs for identifying objects.
|
||||||
|
|
||||||
These are currently used for:
|
These are currently used for:
|
||||||
* delivery destinations, eg ActivityPub inboxes, webmention targets, etc.
|
|
||||||
* copies of :class:`Object`s and :class:`User`s elsewhere, eg at:// URIs for
|
|
||||||
ATProto records, nevent etc bech32-encoded Nostr ids, ATProto user DIDs,
|
|
||||||
etc.
|
|
||||||
|
|
||||||
Used in StructuredPropertys inside :class:`Object` and :class:`User`; not
|
* delivery destinations, eg ActivityPub inboxes, webmention targets, etc.
|
||||||
stored as top-level entities in the datastore.
|
* copies of :class:`Object`\s and :class:`User`\s elsewhere,
|
||||||
|
eg ``at://`` URIs for ATProto records, nevent etc bech32-encoded Nostr ids,
|
||||||
|
ATProto user DIDs, etc.
|
||||||
|
|
||||||
|
Used in :class:`google.cloud.ndb.model.StructuredProperty`\s inside
|
||||||
|
:class:`Object` and :class:`User`\;
|
||||||
|
not stored as top-level entities in the datastore.
|
||||||
|
|
||||||
ndb implements this by hoisting each property here into a corresponding
|
ndb implements this by hoisting each property here into a corresponding
|
||||||
property on the parent entity, prefixed by the StructuredProperty name
|
property on the parent entity, prefixed by the StructuredProperty name
|
||||||
below, eg `delivered.uri`, `delivered.protocol`, etc.
|
below, eg ``delivered.uri``, ``delivered.protocol``, etc.
|
||||||
|
|
||||||
For repeated StructuredPropertys, the hoisted properties are all repeated on
|
For repeated StructuredPropertys, the hoisted properties are all repeated on
|
||||||
the parent entity, and reconstructed into StructuredPropertys based on their
|
the parent entity, and reconstructed into StructuredPropertys based on their
|
||||||
|
@ -87,7 +89,7 @@ class Target(ndb.Model):
|
||||||
|
|
||||||
|
|
||||||
class ProtocolUserMeta(type(ndb.Model)):
|
class ProtocolUserMeta(type(ndb.Model)):
|
||||||
""":class:`User` metaclass. Registers all subclasses in the PROTOCOLS global."""
|
""":class:`User` metaclass. Registers all subclasses in the ``PROTOCOLS`` global."""
|
||||||
def __new__(meta, name, bases, class_dict):
|
def __new__(meta, name, bases, class_dict):
|
||||||
cls = super().__new__(meta, name, bases, class_dict)
|
cls = super().__new__(meta, name, bases, class_dict)
|
||||||
|
|
||||||
|
@ -100,7 +102,7 @@ class ProtocolUserMeta(type(ndb.Model)):
|
||||||
|
|
||||||
|
|
||||||
def reset_protocol_properties():
|
def reset_protocol_properties():
|
||||||
"""Recreates various protocol properties to include choices PROTOCOLS."""
|
"""Recreates various protocol properties to include choices from ``PROTOCOLS``."""
|
||||||
Target.protocol = ndb.StringProperty(
|
Target.protocol = ndb.StringProperty(
|
||||||
'protocol', choices=list(PROTOCOLS.keys()), required=True)
|
'protocol', choices=list(PROTOCOLS.keys()), required=True)
|
||||||
Object.source_protocol = ndb.StringProperty(
|
Object.source_protocol = ndb.StringProperty(
|
||||||
|
@ -119,11 +121,10 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
|
||||||
Stores some protocols' keypairs. Currently:
|
Stores some protocols' keypairs. Currently:
|
||||||
|
|
||||||
* RSA keypair for ActivityPub HTTP Signatures
|
* RSA keypair for ActivityPub HTTP Signatures
|
||||||
properties: mod, public_exponent, private_exponent, all encoded as
|
properties: ``mod``, ``public_exponent``, ``private_exponent``, all
|
||||||
base64url (ie URL-safe base64) strings as described in RFC 4648 and
|
encoded as base64url (ie URL-safe base64) strings as described in RFC
|
||||||
section 5.1 of the Magic Signatures spec
|
4648 and section 5.1 of the Magic Signatures spec:
|
||||||
https://tools.ietf.org/html/draft-cavage-http-signatures-12
|
https://tools.ietf.org/html/draft-cavage-http-signatures-12
|
||||||
|
|
||||||
* *Not* K-256 signing or rotation keys for AT Protocol, those are stored in
|
* *Not* K-256 signing or rotation keys for AT Protocol, those are stored in
|
||||||
:class:`arroba.datastore_storage.AtpRepo` entities
|
:class:`arroba.datastore_storage.AtpRepo` entities
|
||||||
"""
|
"""
|
||||||
|
@ -156,8 +157,9 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
"""Constructor.
|
"""Constructor.
|
||||||
|
|
||||||
Sets :attr:`obj` explicitly because however :class:`Model` sets it
|
Sets :attr:`obj` explicitly because however
|
||||||
doesn't work with @property and @obj.setter below.
|
:class:`google.cloud.ndb.model.Model` sets it doesn't work with
|
||||||
|
``@property`` and ``@obj.setter`` below.
|
||||||
"""
|
"""
|
||||||
obj = kwargs.pop('obj', None)
|
obj = kwargs.pop('obj', None)
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
@ -175,7 +177,9 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_by_id(cls, id):
|
def get_by_id(cls, id):
|
||||||
"""Override Model.get_by_id to follow the use_instead property."""
|
"""Override :meth:`google.cloud.ndb.model.Model.get_by_id` to follow the
|
||||||
|
``use_instead`` property.
|
||||||
|
"""
|
||||||
user = cls._get_by_id(id)
|
user = cls._get_by_id(id)
|
||||||
if user and user.use_instead:
|
if user and user.use_instead:
|
||||||
logger.info(f'{user.key} use_instead => {user.use_instead}')
|
logger.info(f'{user.key} use_instead => {user.use_instead}')
|
||||||
|
@ -187,7 +191,7 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
|
||||||
def get_for_copy(copy_id):
|
def get_for_copy(copy_id):
|
||||||
"""Fetches a user with a given id in copies.
|
"""Fetches a user with a given id in copies.
|
||||||
|
|
||||||
Thin wrapper around :meth:User.get_copies` that returns the first
|
Thin wrapper around :meth:`User.get_copies` that returns the first
|
||||||
matching :class:`User`.
|
matching :class:`User`.
|
||||||
"""
|
"""
|
||||||
users = User.get_for_copies([copy_id])
|
users = User.get_for_copies([copy_id])
|
||||||
|
@ -215,7 +219,7 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
|
||||||
@classmethod
|
@classmethod
|
||||||
@ndb.transactional()
|
@ndb.transactional()
|
||||||
def get_or_create(cls, id, propagate=False, **kwargs):
|
def get_or_create(cls, id, propagate=False, **kwargs):
|
||||||
"""Loads and returns a User. Creates it if necessary.
|
"""Loads and returns a :class:`User`\. Creates it if necessary.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
propagate (bool): whether to create copies of this user in push-based
|
propagate (bool): whether to create copies of this user in push-based
|
||||||
|
@ -280,7 +284,7 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
|
||||||
"""Loads :attr:`obj` for multiple users in parallel.
|
"""Loads :attr:`obj` for multiple users in parallel.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
users: sequence of :class:`User`
|
users (sequence of User)
|
||||||
"""
|
"""
|
||||||
objs = ndb.get_multi(u.obj_key for u in users if u.obj_key)
|
objs = ndb.get_multi(u.obj_key for u in users if u.obj_key)
|
||||||
keys_to_objs = {o.key: o for o in objs if o}
|
keys_to_objs = {o.key: o for o in objs if o}
|
||||||
|
@ -340,13 +344,19 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
|
||||||
return self.handle or self.key.id()
|
return self.handle or self.key.id()
|
||||||
|
|
||||||
def public_pem(self):
|
def public_pem(self):
|
||||||
"""Returns: bytes"""
|
"""
|
||||||
|
Returns:
|
||||||
|
bytes:
|
||||||
|
"""
|
||||||
rsa = RSA.construct((base64_to_long(str(self.mod)),
|
rsa = RSA.construct((base64_to_long(str(self.mod)),
|
||||||
base64_to_long(str(self.public_exponent))))
|
base64_to_long(str(self.public_exponent))))
|
||||||
return rsa.exportKey(format='PEM')
|
return rsa.exportKey(format='PEM')
|
||||||
|
|
||||||
def private_pem(self):
|
def private_pem(self):
|
||||||
"""Returns: bytes"""
|
"""
|
||||||
|
Returns:
|
||||||
|
bytes:
|
||||||
|
"""
|
||||||
assert self.mod and self.public_exponent and self.private_exponent, str(self)
|
assert self.mod and self.public_exponent and self.private_exponent, str(self)
|
||||||
rsa = RSA.construct((base64_to_long(str(self.mod)),
|
rsa = RSA.construct((base64_to_long(str(self.mod)),
|
||||||
base64_to_long(str(self.public_exponent)),
|
base64_to_long(str(self.public_exponent)),
|
||||||
|
@ -354,7 +364,7 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
|
||||||
return rsa.exportKey(format='PEM')
|
return rsa.exportKey(format='PEM')
|
||||||
|
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Returns this user's human-readable name, eg 'Ryan Barrett'."""
|
"""Returns this user's human-readable name, eg ``Ryan Barrett``."""
|
||||||
if self.obj and self.obj.as1:
|
if self.obj and self.obj.as1:
|
||||||
name = self.obj.as1.get('displayName')
|
name = self.obj.as1.get('displayName')
|
||||||
if name:
|
if name:
|
||||||
|
@ -363,7 +373,7 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
|
||||||
return self.handle_or_id()
|
return self.handle_or_id()
|
||||||
|
|
||||||
def web_url(self):
|
def web_url(self):
|
||||||
"""Returns this user's web URL (homepage), eg 'https://foo.com/'.
|
"""Returns this user's web URL (homepage), eg ``https://foo.com/``.
|
||||||
|
|
||||||
To be implemented by subclasses.
|
To be implemented by subclasses.
|
||||||
|
|
||||||
|
@ -376,10 +386,10 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
|
||||||
"""Returns True if the given URL is this user's web URL (homepage).
|
"""Returns True if the given URL is this user's web URL (homepage).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
url: str
|
url (str)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
boolean
|
bool:
|
||||||
"""
|
"""
|
||||||
if not url:
|
if not url:
|
||||||
return False
|
return False
|
||||||
|
@ -399,7 +409,7 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
|
||||||
"""Returns this user's ActivityPub address, eg ``@me@foo.com``.
|
"""Returns this user's ActivityPub address, eg ``@me@foo.com``.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str
|
str:
|
||||||
"""
|
"""
|
||||||
# TODO: use self.handle_as? need it to fall back to id?
|
# TODO: use self.handle_as? need it to fall back to id?
|
||||||
return f'@{self.handle_or_id()}@{self.ABBREV}{common.SUPERDOMAIN}'
|
return f'@{self.handle_or_id()}@{self.ABBREV}{common.SUPERDOMAIN}'
|
||||||
|
@ -407,7 +417,7 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
|
||||||
def ap_actor(self, rest=None):
|
def ap_actor(self, rest=None):
|
||||||
"""Returns this user's ActivityPub/AS2 actor id.
|
"""Returns this user's ActivityPub/AS2 actor id.
|
||||||
|
|
||||||
Eg ``https://atproto.brid.gy/ap/foo.com`.
|
Eg ``https://atproto.brid.gy/ap/foo.com``.
|
||||||
|
|
||||||
May be overridden by subclasses.
|
May be overridden by subclasses.
|
||||||
|
|
||||||
|
@ -435,7 +445,7 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
|
||||||
Defaults to this user's key id.
|
Defaults to this user's key id.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str
|
str:
|
||||||
"""
|
"""
|
||||||
return self.key.id()
|
return self.key.id()
|
||||||
|
|
||||||
|
@ -516,11 +526,10 @@ class Object(StringIdModel):
|
||||||
|
|
||||||
new = None
|
new = None
|
||||||
changed = None
|
changed = None
|
||||||
"""
|
"""Protocol and subclasses set these in fetch if this :class:`Object` is
|
||||||
Protocol and subclasses set these in fetch if this Object is new or if its
|
new or if its contents have changed from what was originally loaded from the
|
||||||
contents have changed from what was originally loaded from the datastore.
|
datastore. If either one is None, that means we don't know whether this
|
||||||
If either one is None, that means we don't know whether this Object is
|
:class:`Object` is new/changed.
|
||||||
new/changed.
|
|
||||||
|
|
||||||
:attr:`changed` is populated by :meth:`Object.activity_changed()`.
|
:attr:`changed` is populated by :meth:`Object.activity_changed()`.
|
||||||
"""
|
"""
|
||||||
|
@ -664,12 +673,12 @@ class Object(StringIdModel):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_by_id(cls, id):
|
def get_by_id(cls, id):
|
||||||
"""Override Model.get_by_id to un-escape ^^ to #.
|
"""Override :meth:`google.cloud.ndb.model.Model.get_by_id` to un-escape
|
||||||
|
``^^`` to ``#``.
|
||||||
|
|
||||||
Only needed for compatibility with historical URL paths, we're now back
|
Only needed for compatibility with historical URL paths, we're now back
|
||||||
to URL-encoding #s instead.
|
to URL-encoding ``#``s instead.
|
||||||
https://github.com/snarfed/bridgy-fed/issues/469
|
https://github.com/snarfed/bridgy-fed/issues/469
|
||||||
|
|
||||||
See "meth:`proxy_url()` for the inverse.
|
See "meth:`proxy_url()` for the inverse.
|
||||||
"""
|
"""
|
||||||
return super().get_by_id(id.replace('^^', '#'))
|
return super().get_by_id(id.replace('^^', '#'))
|
||||||
|
@ -677,14 +686,14 @@ class Object(StringIdModel):
|
||||||
@classmethod
|
@classmethod
|
||||||
@ndb.transactional()
|
@ndb.transactional()
|
||||||
def get_or_create(cls, id, **props):
|
def get_or_create(cls, id, **props):
|
||||||
"""Returns an Object with the given property values.
|
"""Returns an :class:`Object` with the given property values.
|
||||||
|
|
||||||
If a matching Object doesn't exist in the datastore, creates it first.
|
If a matching :class:`Object` doesn't exist in the datastore, creates it
|
||||||
Only populates non-False/empty property values in props into the object.
|
first. Only populates non-False/empty property values in props into the
|
||||||
Also populates the :attr:`new` and :attr:`changed` properties.
|
object. Also populates the :attr:`new` and :attr:`changed` properties.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
:class:`Object`
|
Object:
|
||||||
"""
|
"""
|
||||||
obj = cls.get_by_id(id)
|
obj = cls.get_by_id(id)
|
||||||
if obj:
|
if obj:
|
||||||
|
@ -723,8 +732,8 @@ class Object(StringIdModel):
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
fetch_blobs (bool): whether to fetch images and other blobs, store
|
fetch_blobs (bool): whether to fetch images and other blobs, store
|
||||||
them in :class:`arroba.AtpRemoteBlob'\s if they don't already exist,
|
them in :class:`arroba.datastore_storage.AtpRemoteBlob`\s if they
|
||||||
and fill them into the returned object.
|
don't already exist, and fill them into the returned object.
|
||||||
"""
|
"""
|
||||||
if self.bsky:
|
if self.bsky:
|
||||||
return self.bsky
|
return self.bsky
|
||||||
|
@ -744,14 +753,14 @@ class Object(StringIdModel):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def activity_changed(self, other_as1):
|
def activity_changed(self, other_as1):
|
||||||
"""Returns True if this activity is meaningfully changed from other_as1.
|
"""Returns True if this activity is meaningfully changed from ``other_as1``.
|
||||||
|
|
||||||
...otherwise False.
|
...otherwise False.
|
||||||
|
|
||||||
Used to populate :attr:`changed`.
|
Used to populate :attr:`changed`.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
other_as1: dict AS1 object, or none
|
other_as1 (dict): AS1 object, or none
|
||||||
"""
|
"""
|
||||||
return (as1.activity_changed(self.as1, other_as1)
|
return (as1.activity_changed(self.as1, other_as1)
|
||||||
if self.as1 and other_as1
|
if self.as1 and other_as1
|
||||||
|
@ -760,10 +769,11 @@ class Object(StringIdModel):
|
||||||
def proxy_url(self):
|
def proxy_url(self):
|
||||||
"""Returns the Bridgy Fed proxy URL to render this post as HTML.
|
"""Returns the Bridgy Fed proxy URL to render this post as HTML.
|
||||||
|
|
||||||
Note that some webmention receivers are struggling with the %23s
|
Note that some webmention receivers are struggling with the ``%23``s
|
||||||
(URL-encoded #s) in these paths:
|
(URL-encoded ``#``s) in these paths:
|
||||||
https://github.com/snarfed/bridgy-fed/issues/469
|
|
||||||
https://github.com/pfefferle/wordpress-webmention/issues/359
|
* https://github.com/snarfed/bridgy-fed/issues/469
|
||||||
|
* https://github.com/pfefferle/wordpress-webmention/issues/359
|
||||||
|
|
||||||
See "meth:`get_by_id()` for the inverse.
|
See "meth:`get_by_id()` for the inverse.
|
||||||
"""
|
"""
|
||||||
|
@ -845,16 +855,17 @@ class Follower(ndb.Model):
|
||||||
@classmethod
|
@classmethod
|
||||||
@ndb.transactional()
|
@ndb.transactional()
|
||||||
def get_or_create(cls, *, from_, to, **kwargs):
|
def get_or_create(cls, *, from_, to, **kwargs):
|
||||||
"""Returns a Follower with the given from_ and to users.
|
"""Returns a Follower with the given ``from_`` and ``to`` users.
|
||||||
|
|
||||||
If a matching Follower doesn't exist in the datastore, creates it first.
|
If a matching :class:`Follower` doesn't exist in the datastore, creates
|
||||||
|
it first.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
from_: :class:`User`
|
from_ (User)
|
||||||
to: :class:`User`
|
to (User)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
:class:`Follower`
|
Follower:
|
||||||
"""
|
"""
|
||||||
assert from_
|
assert from_
|
||||||
assert to
|
assert to
|
||||||
|
@ -878,11 +889,11 @@ class Follower(ndb.Model):
|
||||||
def fetch_page(collection):
|
def fetch_page(collection):
|
||||||
"""Fetches a page of Followers for the current user.
|
"""Fetches a page of Followers for the current user.
|
||||||
|
|
||||||
Wraps :func:`fetch_page`. Paging uses the `before` and `after` query
|
Wraps :func:`fetch_page`. Paging uses the ``before`` and ``after`` query
|
||||||
parameters, if available in the request.
|
parameters, if available in the request.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
collection, str, 'followers' or 'following'
|
collection (str): ``followers`` or ``following``
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(followers, new_before, new_after) tuple with:
|
(followers, new_before, new_after) tuple with:
|
||||||
|
@ -913,22 +924,22 @@ class Follower(ndb.Model):
|
||||||
def fetch_page(query, model_class):
|
def fetch_page(query, model_class):
|
||||||
"""Fetches a page of results from a datastore query.
|
"""Fetches a page of results from a datastore query.
|
||||||
|
|
||||||
Uses the `before` and `after` query params (if provided; should be ISO8601
|
Uses the ``before`` and ``after`` query params (if provided; should be
|
||||||
timestamps) and the queried model class's `updated` property to identify the
|
ISO8601 timestamps) and the queried model class's ``updated`` property to
|
||||||
page to fetch.
|
identify the page to fetch.
|
||||||
|
|
||||||
Populates a `log_url_path` property on each result entity that points to a
|
Populates a ``log_url_path`` property on each result entity that points to a
|
||||||
its most recent logged request.
|
its most recent logged request.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
query: :class:`ndb.Query`
|
query (ndb.Query)
|
||||||
model_class: ndb model class
|
model_class (class)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(results, new_before, new_after) tuple with:
|
(list of entities, str, str) tuple:
|
||||||
results: list of query result entities
|
(results, new_before, new_after), where new_before and new_after are query
|
||||||
new_before, new_after: str query param values for `before` and `after`
|
param values for ``before`` and ``after`` to fetch the previous and next
|
||||||
to fetch the previous and next pages, respectively
|
pages, respectively
|
||||||
"""
|
"""
|
||||||
# if there's a paging param ('before' or 'after'), update query with it
|
# if there's a paging param ('before' or 'after'), update query with it
|
||||||
# TODO: unify this with Bridgy's user page
|
# TODO: unify this with Bridgy's user page
|
||||||
|
|
19
pages.py
19
pages.py
|
@ -35,8 +35,8 @@ def load_user(protocol, id):
|
||||||
"""Loads the current request's user into `g.user`.
|
"""Loads the current request's user into `g.user`.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
protocol: str
|
protocol (str):
|
||||||
id: str
|
id (str):
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:class:`werkzeug.exceptions.HTTPException` on error or redirect
|
:class:`werkzeug.exceptions.HTTPException` on error or redirect
|
||||||
|
@ -231,19 +231,18 @@ def bridge_user():
|
||||||
|
|
||||||
|
|
||||||
def fetch_objects(query):
|
def fetch_objects(query):
|
||||||
"""Fetches a page of Object entities from a datastore query.
|
"""Fetches a page of :class:`models.Object` entities from a datastore query.
|
||||||
|
|
||||||
Wraps :func:`models.fetch_page` and adds attributes to the returned Object
|
Wraps :func:`models.fetch_page` and adds attributes to the returned
|
||||||
entities for rendering in objects.html.
|
:class:`models.Object` entities for rendering in ``objects.html``.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
query: :class:`ndb.Query`
|
query (ndb.Query)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(results, new_before, new_after) tuple with:
|
(list of models.Object, str, str) tuple:
|
||||||
results: list of Object entities
|
(results, new ``before`` query param, new ``after`` query param)
|
||||||
new_before, new_after: str query param values for `before` and `after`
|
to fetch the previous and next pages, respectively
|
||||||
to fetch the previous and next pages, respectively
|
|
||||||
"""
|
"""
|
||||||
objects, new_before, new_after = fetch_page(query, Object)
|
objects, new_before, new_after = fetch_page(query, Object)
|
||||||
|
|
||||||
|
|
113
protocol.py
113
protocol.py
|
@ -52,9 +52,9 @@ class Protocol:
|
||||||
"""Base protocol class. Not to be instantiated; classmethods only.
|
"""Base protocol class. Not to be instantiated; classmethods only.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
LABEL: str, human-readable lower case name
|
LABEL (str): human-readable lower case name
|
||||||
OTHER_LABELS: sequence of str, label aliases
|
OTHER_LABELS (sequence): of str, label aliases
|
||||||
ABBREV: str, lower case abbreviation, used in URL paths
|
ABBREV (str): lower case abbreviation, used in URL paths
|
||||||
"""
|
"""
|
||||||
ABBREV = None
|
ABBREV = None
|
||||||
OTHER_LABELS = ()
|
OTHER_LABELS = ()
|
||||||
|
@ -74,13 +74,13 @@ class Protocol:
|
||||||
...based on the request's hostname.
|
...based on the request's hostname.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
fed (str or Protocol): protocol to return if the current request is on
|
fed (str or protocol.Protocol): protocol to return if the current
|
||||||
``fed.brid.gy``
|
request is on ``fed.brid.gy``
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Protocol subclass: ...or None if the provided domain or request
|
protocol.Protocol subclass: protocol, or None if the provided domain
|
||||||
hostname domain is not a subdomain of ``brid.gy` or isn't a known
|
or request hostname domain is not a subdomain of ``brid.gy` or isn't
|
||||||
protocol
|
a known protocol
|
||||||
"""
|
"""
|
||||||
return Protocol.for_bridgy_subdomain(request.host, fed=fed)
|
return Protocol.for_bridgy_subdomain(request.host, fed=fed)
|
||||||
|
|
||||||
|
@ -89,14 +89,13 @@ class Protocol:
|
||||||
"""Returns the protocol for a brid.gy subdomain.
|
"""Returns the protocol for a brid.gy subdomain.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
domain_or_url: str
|
domain_or_url (str)
|
||||||
fed (str or Protocol): protocol to return if the current request is on
|
fed (str or protocol.Protocol): protocol to return if the current
|
||||||
``fed.brid.gy``
|
request is on ``fed.brid.gy``
|
||||||
|
|
||||||
Returns:
|
Returns: protocol.Protocol subclass: protocol, or None if the provided
|
||||||
Protocol subclass: ...or None if the provided domain or request
|
domain or request hostname domain is not a subdomain of ``brid.gy` or
|
||||||
hostname domain is not a subdomain of ``brid.gy` or isn't a known
|
isn't a known protocol
|
||||||
protocol
|
|
||||||
"""
|
"""
|
||||||
domain = (util.domain_from_link(domain_or_url, minimize=False)
|
domain = (util.domain_from_link(domain_or_url, minimize=False)
|
||||||
if util.is_web(domain_or_url)
|
if util.is_web(domain_or_url)
|
||||||
|
@ -116,10 +115,10 @@ class Protocol:
|
||||||
'https://ap.brid.gy/foo/bar'.
|
'https://ap.brid.gy/foo/bar'.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path: str
|
path (str)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str, URL
|
str: URL
|
||||||
"""
|
"""
|
||||||
return urljoin(f'https://{cls.ABBREV or "fed"}{common.SUPERDOMAIN}/', path)
|
return urljoin(f'https://{cls.ABBREV or "fed"}{common.SUPERDOMAIN}/', path)
|
||||||
|
|
||||||
|
@ -196,7 +195,7 @@ class Protocol:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def key_for(cls, id):
|
def key_for(cls, id):
|
||||||
"""Returns the :class:`ndb.Key` for a given id's :class:`User`.
|
"""Returns the :class:`ndb.Key` for a given id's :class:`models.User`.
|
||||||
|
|
||||||
To be implemented by subclasses. Canonicalizes the id if necessary.
|
To be implemented by subclasses. Canonicalizes the id if necessary.
|
||||||
|
|
||||||
|
@ -344,8 +343,8 @@ class Protocol:
|
||||||
default_g_user is True, otherwise None.
|
default_g_user is True, otherwise None.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj: :class:`Object`
|
obj (models.Object)
|
||||||
default_g_user: boolean
|
default_g_user (bool)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
:class:`ndb.Key` or None
|
:class:`ndb.Key` or None
|
||||||
|
@ -363,9 +362,9 @@ class Protocol:
|
||||||
To be implemented by subclasses.
|
To be implemented by subclasses.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj: :class:`Object` with activity to send
|
obj (models.Object): with activity to send
|
||||||
url: str, destination URL to send to
|
url (str): destination URL to send to
|
||||||
log_data: boolean, whether to log full data object
|
log_data (bool): whether to log full data object
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if the activity is sent successfully, False if it is ignored or
|
True if the activity is sent successfully, False if it is ignored or
|
||||||
|
@ -389,7 +388,7 @@ class Protocol:
|
||||||
To be implemented by subclasses.
|
To be implemented by subclasses.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj: :class:`Object` with the id to fetch. Data is filled into one of
|
obj (models.Object): with the id to fetch. Data is filled into one of
|
||||||
the protocol-specific properties, eg as2, mf2, bsky.
|
the protocol-specific properties, eg as2, mf2, bsky.
|
||||||
**kwargs: subclass-specific
|
**kwargs: subclass-specific
|
||||||
|
|
||||||
|
@ -413,7 +412,7 @@ class Protocol:
|
||||||
To be implemented by subclasses.
|
To be implemented by subclasses.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj: :class:`Object`
|
obj (models.Object):
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(response body, dict with HTTP headers) tuple appropriate to be
|
(response body, dict with HTTP headers) tuple appropriate to be
|
||||||
|
@ -436,8 +435,8 @@ class Protocol:
|
||||||
inbox.
|
inbox.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj: :class:`Object`
|
obj (models.Object):
|
||||||
shared: boolean, optional. If `True`, returns a common/shared
|
shared (bool): optional. If `True`, returns a common/shared
|
||||||
endpoint, eg ActivityPub's `sharedInbox`, that can be reused for
|
endpoint, eg ActivityPub's `sharedInbox`, that can be reused for
|
||||||
multiple recipients for efficiency
|
multiple recipients for efficiency
|
||||||
|
|
||||||
|
@ -453,9 +452,9 @@ class Protocol:
|
||||||
Default implementation here, subclasses may override.
|
Default implementation here, subclasses may override.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
url: str
|
url (str):
|
||||||
|
|
||||||
Returns: boolean
|
Returns: bool
|
||||||
"""
|
"""
|
||||||
return util.domain_or_parent_in(util.domain_from_link(url),
|
return util.domain_or_parent_in(util.domain_from_link(url),
|
||||||
DOMAIN_BLOCKLIST + DOMAINS)
|
DOMAIN_BLOCKLIST + DOMAINS)
|
||||||
|
@ -468,7 +467,7 @@ class Protocol:
|
||||||
raises :class:`werkzeug.exceptions.BadRequest`.
|
raises :class:`werkzeug.exceptions.BadRequest`.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj: :class:`Object`
|
obj (models.Object):
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(response body, HTTP status code) tuple for Flask response
|
(response body, HTTP status code) tuple for Flask response
|
||||||
|
@ -639,7 +638,7 @@ class Protocol:
|
||||||
"""Handles an incoming follow activity.
|
"""Handles an incoming follow activity.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj: :class:`Object`, follow activity
|
obj (models.Object): follow activity
|
||||||
"""
|
"""
|
||||||
logger.info('Got follow. Loading users, storing Follow(s), sending accept(s)')
|
logger.info('Got follow. Loading users, storing Follow(s), sending accept(s)')
|
||||||
|
|
||||||
|
@ -732,11 +731,10 @@ class Protocol:
|
||||||
Checks if we've seen it before.
|
Checks if we've seen it before.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj: :class:`Object`
|
obj (models.Object)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
obj: :class:`Object`, the same one if the input obj is an activity,
|
Object: ``obj`` if it's an activity, otherwise a new object
|
||||||
otherwise a new one
|
|
||||||
"""
|
"""
|
||||||
if obj.type not in ('note', 'article', 'comment'):
|
if obj.type not in ('note', 'article', 'comment'):
|
||||||
return obj
|
return obj
|
||||||
|
@ -795,7 +793,7 @@ class Protocol:
|
||||||
"""Delivers an activity to its external recipients.
|
"""Delivers an activity to its external recipients.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj: :class:`Object`, activity to deliver
|
obj (models.Object): activity to deliver
|
||||||
"""
|
"""
|
||||||
# find delivery targets
|
# find delivery targets
|
||||||
# sort targets so order is deterministic for tests, debugging, etc
|
# sort targets so order is deterministic for tests, debugging, etc
|
||||||
|
@ -865,13 +863,11 @@ class Protocol:
|
||||||
Targets are both objects - original posts, events, etc - and actors.
|
Targets are both objects - original posts, events, etc - and actors.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj (:class:`models.Object`)
|
obj (models.Object)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: {
|
dict: maps :class:`Target`: to original (in response to)
|
||||||
:class:`Target`: original (in response to) :class:`models.Object`,
|
:class:`models.Object`, if any, otherwise None
|
||||||
if any, otherwise None
|
|
||||||
}
|
|
||||||
"""
|
"""
|
||||||
logger.info('Finding recipients and their targets')
|
logger.info('Finding recipients and their targets')
|
||||||
|
|
||||||
|
@ -1000,24 +996,25 @@ class Protocol:
|
||||||
Note that :meth:`Object._post_put_hook` updates the cache.
|
Note that :meth:`Object._post_put_hook` updates the cache.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
id: str
|
id (str)
|
||||||
|
remote (bool): whether to fetch the object over the network. If True,
|
||||||
remote: boolean, whether to fetch the object over the network. If True,
|
|
||||||
fetches even if we already have the object stored, and updates our
|
fetches even if we already have the object stored, and updates our
|
||||||
stored copy. If False and we don't have the object stored, returns
|
stored copy. If False and we don't have the object stored, returns
|
||||||
None. Default (None) means to fetch over the network only if we
|
None. Default (None) means to fetch over the network only if we
|
||||||
don't already have it stored.
|
don't already have it stored.
|
||||||
local: boolean, whether to load from the datastore before
|
local (bool): whether to load from the datastore before
|
||||||
fetching over the network. If False, still stores back to the
|
fetching over the network. If False, still stores back to the
|
||||||
datastore after a successful remote fetch.
|
datastore after a successful remote fetch.
|
||||||
kwargs: passed through to :meth:`fetch()`
|
kwargs: passed through to :meth:`fetch()`
|
||||||
|
|
||||||
Returns: :class:`Object`, or None if:
|
Returns
|
||||||
|
models.Object: loaded object, or None if:
|
||||||
|
|
||||||
* it isn't fetchable, eg a non-URL string for Web
|
* it isn't fetchable, eg a non-URL string for Web
|
||||||
* remote is False and it isn't in the cache or datastore
|
* ``remote`` is False and it isn't in the cache or datastore
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:class:`requests.HTTPError`, anything else that :meth:`fetch` raises
|
requests.HTTPError: anything that :meth:`fetch` raises
|
||||||
"""
|
"""
|
||||||
assert local or remote is not False
|
assert local or remote is not False
|
||||||
|
|
||||||
|
@ -1081,19 +1078,19 @@ class Protocol:
|
||||||
|
|
||||||
@app.post('/queue/receive')
|
@app.post('/queue/receive')
|
||||||
def receive_task():
|
def receive_task():
|
||||||
"""Task handler for a newly received :class:`Object`.
|
"""Task handler for a newly received :class:`models.Object`.
|
||||||
|
|
||||||
Form parameters:
|
Parameters:
|
||||||
|
* obj (ndb.Key): :class:`models.Object` to handle
|
||||||
|
* user (ndb.Key): :class:`models.User` this activity is on behalf of. This
|
||||||
|
user will be loaded into ``g.user``
|
||||||
|
|
||||||
* obj: urlsafe :class:`ndb.Key` of the :class:`Object` to handle
|
TODO: migrate incoming webmentions and AP inbox deliveries to this. The
|
||||||
* user: urlsafe :class:`ndb.Key` of the :class:`User` this activity is on
|
difficulty is that parts of :meth:`protocol.Protocol.receive` depend on
|
||||||
behalf of. This user will be loaded into `g.user`.
|
setup in :func:`web.webmention` and :func:`activitypub.inbox`, eg
|
||||||
|
:class:`models.Object` with ``new`` and ``changed``, ``g.user`` (which
|
||||||
TODO: migrate incoming webmentions and AP inbox deliveries to this.
|
:meth:`receive` now loads), HTTP request details, etc. See stash for attempt
|
||||||
difficulty is that parts of Protocol.receive depend on setup in
|
at this for :class:`web.Web`.
|
||||||
Web.webmention and ActivityPub.inbox, eg Object with new/changed, g.user
|
|
||||||
(which receive now loads), HTTP request details, etc. see stash for attempt
|
|
||||||
at this for Web.
|
|
||||||
"""
|
"""
|
||||||
logger.info(f'Params: {list(request.form.items())}')
|
logger.info(f'Params: {list(request.form.items())}')
|
||||||
|
|
||||||
|
|
24
redirect.py
24
redirect.py
|
@ -1,18 +1,18 @@
|
||||||
"""Simple conneg endpoint that serves AS2 or redirects to to the original post.
|
"""Simple conneg endpoint that serves AS2 or redirects to to the original post.
|
||||||
|
|
||||||
Only for Web users. Other protocols (including Web sometimes) use /convert/ in
|
Only for :class:`web.Web` users. Other protocols (including :class:`web.Web`
|
||||||
convert.py instead.
|
sometimes) use ``/`` convert ``/`` in convert.py instead.
|
||||||
|
|
||||||
Serves /r/https://foo.com/bar URL paths, where https://foo.com/bar is a original
|
Serves ``/r/https://foo.com/bar`` URL paths, where ``https://foo.com/bar`` is a
|
||||||
post for a Web user. Needed for Mastodon interop, they require that AS2 object
|
original post for a :class:`Web` user. Needed for Mastodon interop, they require
|
||||||
ids and urls are on the same domain that serves them. Background:
|
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/snarfed/bridgy-fed/issues/16#issuecomment-424799599
|
||||||
https://github.com/tootsuite/mastodon/pull/6219#issuecomment-429142747
|
* https://github.com/tootsuite/mastodon/pull/6219#issuecomment-429142747
|
||||||
|
|
||||||
The conneg makes these /r/ URLs searchable in Mastodon:
|
The conneg makes these ``/r/`` URLs searchable in Mastodon:
|
||||||
https://github.com/snarfed/bridgy-fed/issues/352
|
https://github.com/snarfed/bridgy-fed/issues/352
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
@ -47,9 +47,9 @@ DOMAIN_ALLOWLIST = frozenset((
|
||||||
def redir(to):
|
def redir(to):
|
||||||
"""Either redirect to a given URL or convert it to another format.
|
"""Either redirect to a given URL or convert it to another format.
|
||||||
|
|
||||||
E.g. redirects /r/https://foo.com/bar?baz to https://foo.com/bar?baz, or if
|
E.g. redirects ``/r/https://foo.com/bar?baz`` to
|
||||||
it's requested with AS2 conneg in the Accept header, fetches and converts
|
``https://foo.com/bar?baz``, or if it's requested with AS2 conneg in the
|
||||||
and serves it as AS2.
|
``Accept`` header, fetches and converts and serves it as AS2.
|
||||||
"""
|
"""
|
||||||
if request.args:
|
if request.args:
|
||||||
to += '?' + urllib.parse.urlencode(request.args)
|
to += '?' + urllib.parse.urlencode(request.args)
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
# coding=utf-8
|
|
||||||
"""Unit tests for activitypub.py."""
|
"""Unit tests for activitypub.py."""
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
import copy
|
import copy
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
# coding=utf-8
|
|
||||||
"""Unit tests for models.py."""
|
"""Unit tests for models.py."""
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
# coding=utf-8
|
|
||||||
"""Unit tests for webmention.py."""
|
"""Unit tests for webmention.py."""
|
||||||
import copy
|
import copy
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
26
web.py
26
web.py
|
@ -1,4 +1,4 @@
|
||||||
"""Handles inbound webmentions."""
|
"""Webmention protocol with microformats2 in HTML, aka the IndieWeb stack."""
|
||||||
import datetime
|
import datetime
|
||||||
import difflib
|
import difflib
|
||||||
import logging
|
import logging
|
||||||
|
@ -109,7 +109,7 @@ class Web(User, Protocol):
|
||||||
profile_id = web_url
|
profile_id = web_url
|
||||||
|
|
||||||
def ap_address(self):
|
def ap_address(self):
|
||||||
"""Returns this user's ActivityPub address, eg '@foo.com@foo.com'.
|
"""Returns this user's ActivityPub address, eg ``@foo.com@foo.com``.
|
||||||
|
|
||||||
Uses the user's domain if they're direct, fed.brid.gy if they're not.
|
Uses the user's domain if they're direct, fed.brid.gy if they're not.
|
||||||
"""
|
"""
|
||||||
|
@ -147,7 +147,8 @@ class Web(User, Protocol):
|
||||||
|
|
||||||
Uses stored representative h-card if available, falls back to id.
|
Uses stored representative h-card if available, falls back to id.
|
||||||
|
|
||||||
Returns: str
|
Returns:
|
||||||
|
str:
|
||||||
"""
|
"""
|
||||||
id = self.key.id()
|
id = self.key.id()
|
||||||
|
|
||||||
|
@ -171,9 +172,9 @@ class Web(User, Protocol):
|
||||||
"""Fetches site a couple ways to check for redirects and h-card.
|
"""Fetches site a couple ways to check for redirects and h-card.
|
||||||
|
|
||||||
|
|
||||||
Returns: :class:`Web` that was verified. May be different than
|
Returns:
|
||||||
self! eg if self's domain started with www and we switch to the root
|
web.Web: user that was verified. May be different than self! eg if
|
||||||
domain.
|
self 's domain started with www and we switch to the root domain.
|
||||||
"""
|
"""
|
||||||
domain = self.key.id()
|
domain = self.key.id()
|
||||||
logger.info(f'Verifying {domain}')
|
logger.info(f'Verifying {domain}')
|
||||||
|
@ -333,16 +334,17 @@ class Web(User, Protocol):
|
||||||
def fetch(cls, obj, gateway=False, check_backlink=False, **kwargs):
|
def fetch(cls, obj, gateway=False, check_backlink=False, **kwargs):
|
||||||
"""Fetches a URL over HTTP and extracts its microformats2.
|
"""Fetches a URL over HTTP and extracts its microformats2.
|
||||||
|
|
||||||
Follows redirects, but doesn't change the original URL in obj's id! The
|
Follows redirects, but doesn't change the original URL in ``obj``'s id!
|
||||||
:class:`Model` class doesn't allow that anyway, but more importantly, we
|
The :class:`Model` class doesn't allow that anyway, but more
|
||||||
want to preserve that original URL becase other objects may refer to it
|
importantly, we want to preserve that original URL becase other objects
|
||||||
instead of the final redirect destination URL.
|
may refer to it instead of the final redirect destination URL.
|
||||||
|
|
||||||
See :meth:`Protocol.fetch` for other background.
|
See :meth:`Protocol.fetch` for other background.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
gateway: passed through to :func:`webutil.util.fetch_mf2`
|
gateway (bool): passed through to
|
||||||
check_backlink: bool, optional, whether to require a link to Bridgy
|
:func:`oauth_dropins.webutil.util.fetch_mf2`
|
||||||
|
check_backlink (bool): optional, whether to require a link to Bridgy
|
||||||
Fed. Ignored if the URL is a homepage, ie has no path.
|
Fed. Ignored if the URL is a homepage, ie has no path.
|
||||||
kwargs: ignored
|
kwargs: ignored
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -157,7 +157,7 @@ class Webfinger(flask_util.XrdOrJrd):
|
||||||
|
|
||||||
|
|
||||||
class HostMeta(flask_util.XrdOrJrd):
|
class HostMeta(flask_util.XrdOrJrd):
|
||||||
"""Renders and serves the /.well-known/host-meta file.
|
"""Renders and serves the ``/.well-known/host-meta`` file.
|
||||||
|
|
||||||
Supports both JRD and XRD; defaults to XRD.
|
Supports both JRD and XRD; defaults to XRD.
|
||||||
https://tools.ietf.org/html/rfc6415#section-3
|
https://tools.ietf.org/html/rfc6415#section-3
|
||||||
|
@ -173,7 +173,7 @@ class HostMeta(flask_util.XrdOrJrd):
|
||||||
|
|
||||||
@app.get('/.well-known/host-meta.xrds')
|
@app.get('/.well-known/host-meta.xrds')
|
||||||
def host_meta_xrds():
|
def host_meta_xrds():
|
||||||
"""Renders and serves the /.well-known/host-meta.xrds XRDS-Simple file."""
|
"""Renders and serves the ``/.well-known/host-meta.xrds`` XRDS-Simple file."""
|
||||||
return (render_template('host-meta.xrds', host_uri=common.host_url()),
|
return (render_template('host-meta.xrds', host_uri=common.host_url()),
|
||||||
{'Content-Type': 'application/xrds+xml'})
|
{'Content-Type': 'application/xrds+xml'})
|
||||||
|
|
||||||
|
@ -187,8 +187,8 @@ def fetch(addr):
|
||||||
returning None
|
returning None
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
addr (str): a Webfinger-compatible address, eg @x@y, acct:x@y, or
|
addr (str): a Webfinger-compatible address, eg ``@x@y``, ``acct:x@y``, or
|
||||||
https://x/y
|
``https://x/y``
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: fetched WebFinger data, or None on error
|
dict: fetched WebFinger data, or None on error
|
||||||
|
|
Ładowanie…
Reference in New Issue