docs: fix docstring formatting, other tweaks

pull/650/head
Ryan Barrett 2023-10-05 23:32:31 -07:00
rodzic 6442acb244
commit db29ad7757
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
18 zmienionych plików z 292 dodań i 262 usunięć

Wyświetl plik

@ -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

Wyświetl plik

@ -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
""" """

Wyświetl plik

@ -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}'

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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 ------------------------------------------------------------

Wyświetl plik

@ -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__

Wyświetl plik

@ -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
Wyświetl plik

@ -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
Wyświetl plik

@ -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

Wyświetl plik

@ -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)

Wyświetl plik

@ -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())}')

Wyświetl plik

@ -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)

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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
Wyświetl plik

@ -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
""" """

Wyświetl plik

@ -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