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.
TODO: normalize scheme and domain to lower case. Add that to
:class:`util.UrlCanonicalizer`?
:class:`oauth_dropins.webutil.util.UrlCanonicalizer`\?
"""
super()._pre_put_hook()
id = self.key.id()
@ -149,7 +149,7 @@ class ActivityPub(User, Protocol):
@classmethod
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
# https://indieweb.social/users/bismark has source_protocol webmention
# 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):
"""Delivers an activity to an inbox URL.
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`.
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``.
"""
if to_cls.is_blocklisted(url):
logger.info(f'Skipping sending to {url}')
@ -213,47 +213,48 @@ class ActivityPub(User, Protocol):
def fetch(cls, obj, **kwargs):
"""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
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/23
https://socialhub.activitypub.rocks/t/s2s-create-activity/1647/5
https://github.com/mastodon/mastodon/issues/13879 (open!)
https://github.com/w3c/activitypub/issues/224
* 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/s2s-create-activity/1647/5
* https://github.com/mastodon/mastodon/issues/13879 (open!)
* https://github.com/w3c/activitypub/issues/224
Uses HTTP content negotiation via the Content-Type header. If the url is
HTML and it has a rel-alternate link with an AS2 content type, fetches and
returns that URL.
Uses HTTP content negotiation via the ``Content-Type`` header. If the
url is HTML and it has a ``rel-alternate`` link with an AS2 content
type, fetches and returns that URL.
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://docs.joinmastodon.org/admin/config/#authorized_fetch
* 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://docs.joinmastodon.org/admin/config/#authorized_fetch
Signs the request with the current user's key. If not provided, defaults to
using @snarfed.org@snarfed.org's key.
See :meth:`Protocol.fetch` for more details.
See :meth:`protocol.Protocol.fetch` for more details.
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.
kwargs: ignored
Returns:
True if the object was fetched and populated successfully,
bool: True if the object was fetched and populated successfully,
False otherwise
Raises:
:class:`requests.HTTPError`, :class:`werkzeug.exceptions.HTTPException`
If we raise a werkzeug HTTPException, it will have an additional
requests_response attribute with the last requests.Response we received.
requests.HTTPError:
werkzeug.exceptions.HTTPException: will have an additional
``requests_response`` attribute with the last
:class:`requests.Response` we received.
"""
url = obj.key.id()
if not util.is_web(url):
@ -329,7 +330,7 @@ class ActivityPub(User, Protocol):
"""Verifies the current request's HTTP Signature.
Args:
activity: dict, AS2 activity
activity (dict): AS2 activity
Logs details of the result. Raises :class:`werkzeug.HTTPError` if the
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):
"""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.
Args:
fn: :func:`util.requests_get` or :func:`util.requests_get`
url: str
data: optional AS2 object
log_data: boolean, whether to log full data object
fn (callable): :func:`util.requests_get` or :func:`util.requests_get`
url (str):
data (dict): optional AS2 object
log_data (bool): whether to log full data object
kwargs: passed through to requests
Returns: :class:`requests.Response`
Returns:
requests.Response:
"""
if headers is None:
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):
"""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:
activity: dict, AS2 object or activity
orig_obj: dict, AS2 object, optional. The target of activity's inReplyTo or
activity (dict): AS2 object or activity
orig_obj (dict): AS2 object, optional. The target of activity's inReplyTo or
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):
return activity
@ -650,8 +654,8 @@ def postprocess_as2_actor(actor, wrap=True):
Modifies actor in place.
Args:
actor: dict, AS2 actor object
wrap: boolean, whether to wrap url
actor (dict): AS2 actor object
wrap (bool): whether to wrap url
Returns:
actor dict

Wyświetl plik

@ -122,10 +122,10 @@ class ATProto(User, Protocol):
returning Bridgy Fed's URL as the PDS.
Args:
obj: :class:`Object`
obj (Object)
Returns:
str
str:
"""
id = obj.key.id()
if id.startswith('did:'):
@ -169,10 +169,10 @@ class ATProto(User, Protocol):
def _pds_for(cls, did_obj):
"""
Args:
did_obj: :class:`Object`
did_obj (Object)
Returns:
str, PDS URL, or None
str: PDS URL, or None
"""
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.
Args:
user (User)
user (models.User)
"""
assert not isinstance(user, ATProto)
@ -321,12 +321,12 @@ class ATProto(User, Protocol):
"""Tries to fetch a ATProto object.
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.
kwargs: ignored
Returns:
True if the object was fetched and populated successfully,
bool: True if the object was fetched and populated successfully,
False otherwise
Raises:
@ -364,12 +364,13 @@ class ATProto(User, Protocol):
@classmethod
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
`com.atproto.sync.subscribeRepos` subscriptions, not BF-specific
/convert/... HTTP requests, so this should never be used in practice.
``com.atproto.sync.subscribeRepos`` subscriptions, not BF-specific
``/convert/...`` HTTP requests, so this should never be used in
practice.
"""
return bluesky.from_as1(obj.as1), {'Content-Type': 'application/json'}
@ -378,7 +379,7 @@ class ATProto(User, Protocol):
def poll_notifications():
"""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
"""

Wyświetl plik

@ -1,6 +1,4 @@
# coding=utf-8
"""Misc common utilities.
"""
"""Misc common utilities."""
import base64
from datetime import timedelta
import logging
@ -75,18 +73,18 @@ TASKS_LOCATION = 'us-central1'
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`.
"""
return number.bytes_to_long(base64.urlsafe_b64decode(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))
@ -103,22 +101,22 @@ def host_url(path_query=None):
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)
abort(status, response=make_response({'error': msg}, status), **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
https://mastodon.social/users/foo, defaults text to @foo@mastodon.social if
it's not provided.
Args:
url: str
text: str
kwargs: passed through to :func:`webutil.util.pretty_link`
url (str)
text (str)
kwargs: passed through to :func:`oauth_dropins.webutil.util.pretty_link`
"""
if g.user and g.user.is_web_url(url):
return g.user.user_page_link()
@ -144,12 +142,13 @@ def redirect_wrap(url):
...to satisfy Mastodon's non-standard domain matching requirement. :(
Args:
url: string
url (str)
* https://github.com/snarfed/bridgy-fed/issues/16#issuecomment-424799599
* 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:
return url
@ -160,15 +159,16 @@ def redirect_wrap(url):
def redirect_unwrap(val):
"""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.
Strings that aren't wrapped URLs are left unchanged.
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):
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):
"""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
key includes a / at the end, so that we cache webmention endpoints for home
pages separate from other pages. https://github.com/snarfed/bridgy/issues/701
Just the domain by default. If the URL is the home page, ie path is ``/``,
the key includes a ``/`` at the end, so that we cache webmention endpoints
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
Adapted from bridgy/util.py.
Adapted from ``bridgy/util.py``.
"""
parsed = urllib.parse.urlparse(url)
key = parsed.netloc
@ -225,7 +226,7 @@ def webmention_discover(url, **kwargs):
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.
"""
@ -240,13 +241,13 @@ def create_task(queue, **params):
creating a task.
Args:
queue: string, queue name
queue (str): queue name
params: form-encoded and included in the task request body
Returns:
:flask:`Response` from running the task inline if running in a local
server, otherwise (str response body, int status code) response from
creating the task.
flask.Response or (str, int): response from either running the task
inline, if running in a local server, or the response from creating the
task.
"""
assert 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
constants from the :class:`Protocol` subclasses.
URL pattern is ``/convert/SOURCE/DEST``, where ``SOURCE`` and ``DEST`` are the
``LABEL`` constants from the :class:`protocol.Protocol` subclasses.
"""
import logging
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
# 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

Wyświetl plik

@ -342,17 +342,18 @@ texinfo_documents = [
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {
'arroba': ('https://arroba.readthedocs.io/en/latest', None),
'dag_cbor': ('https://dag-cbor.readthedocs.io/en/latest', None),
'arroba': ('https://arroba.readthedocs.io/en/stable', None),
'dag_cbor': ('https://dag-cbor.readthedocs.io/en/stable', None),
'flask': ('https://flask.palletsprojects.com/en/latest', None),
'flask_caching': ('https://flask-caching.readthedocs.io/en/latest', None),
'granary': ('https://granary.readthedocs.io/en/latest', None),
'multiformats': ('https://multiformats.readthedocs.io/en/latest', None),
'oauth_dropins': ('https://oauth-dropins.readthedocs.io/en/latest', None),
'granary': ('https://granary.readthedocs.io/en/stable', None),
'lexrpc': ('https://granary.readthedocs.io/en/stable', 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),
'requests': ('https://requests.readthedocs.io/en/stable/', None),
'urllib3': ('https://urllib3.readthedocs.io/en/latest', None),
'werkzeug': ('https://werkzeug.palletsprojects.com/en/latest/', None),
'requests': ('https://requests.readthedocs.io/en/stable', None),
'urllib3': ('https://urllib3.readthedocs.io/en/stable', None),
'werkzeug': ('https://werkzeug.palletsprojects.com/en/latest', None),
}
# -- Post process ------------------------------------------------------------

Wyświetl plik

@ -8,51 +8,64 @@ Reference documentation.
activitypub
-----------
.. automodule:: activitypub
:exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__
atproto
-------
.. automodule:: atproto
:exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__
common
------
.. automodule:: common
:exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__
convert
-------
.. automodule:: convert
:exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__
follow
------
.. automodule:: follow
:exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__
models
------
.. automodule:: models
:exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__
pages
-----
.. automodule:: pages
:exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__
protocol
--------
.. automodule:: protocol
:exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__
redirect
--------
.. automodule:: redirect
:exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__
render
------
.. automodule:: render
:exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__
superfeedr
----------
.. automodule:: superfeedr
:exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__
web
---
.. automodule:: web
:exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__
webfinger
---------
.. automodule:: webfinger
:exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__

Wyświetl plik

@ -1,8 +1,8 @@
"""Remote follow handler.
https://github.com/snarfed/bridgy-fed/issues/60
https://socialhub.activitypub.rocks/t/what-is-the-current-spec-for-remote-follow/2020
https://www.rfc-editor.org/rfc/rfc7033
* https://github.com/snarfed/bridgy-fed/issues/60
* https://socialhub.activitypub.rocks/t/what-is-the-current-spec-for-remote-follow/2020
* https://www.rfc-editor.org/rfc/rfc7033
"""
import logging

8
ids.py
Wyświetl plik

@ -12,8 +12,8 @@ def convert_id(*, id, from_proto, to_proto):
Args:
id (str)
from_proto (Protocol)
to_proto (Protocol)
from_proto (protocol.Protocol)
to_proto (protocol.Protocol)
Returns:
str: the corresponding id in ``to_proto``
@ -49,8 +49,8 @@ def convert_handle(*, handle, from_proto, to_proto):
Args:
handle (str)
from_proto (Protocol)
to_proto (Protocol)
from_proto (protocol.Protocol)
to_proto (protocol.Protocol)
Returns:
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):
"""Protocol + URI pairs for identifying objects.
""":class:`protocol.Protocol` + URI pairs for identifying objects.
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
stored as top-level entities in the datastore.
* 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 :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
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
the parent entity, and reconstructed into StructuredPropertys based on their
@ -87,7 +89,7 @@ class Target(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):
cls = super().__new__(meta, name, bases, class_dict)
@ -100,7 +102,7 @@ class ProtocolUserMeta(type(ndb.Model)):
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(
'protocol', choices=list(PROTOCOLS.keys()), required=True)
Object.source_protocol = ndb.StringProperty(
@ -119,11 +121,10 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
Stores some protocols' keypairs. Currently:
* RSA keypair for ActivityPub HTTP Signatures
properties: mod, public_exponent, private_exponent, all encoded as
base64url (ie URL-safe base64) strings as described in RFC 4648 and
section 5.1 of the Magic Signatures spec
properties: ``mod``, ``public_exponent``, ``private_exponent``, all
encoded as base64url (ie URL-safe base64) strings as described in RFC
4648 and section 5.1 of the Magic Signatures spec:
https://tools.ietf.org/html/draft-cavage-http-signatures-12
* *Not* K-256 signing or rotation keys for AT Protocol, those are stored in
:class:`arroba.datastore_storage.AtpRepo` entities
"""
@ -156,8 +157,9 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
def __init__(self, **kwargs):
"""Constructor.
Sets :attr:`obj` explicitly because however :class:`Model` sets it
doesn't work with @property and @obj.setter below.
Sets :attr:`obj` explicitly because however
:class:`google.cloud.ndb.model.Model` sets it doesn't work with
``@property`` and ``@obj.setter`` below.
"""
obj = kwargs.pop('obj', None)
super().__init__(**kwargs)
@ -175,7 +177,9 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
@classmethod
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)
if user and 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):
"""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`.
"""
users = User.get_for_copies([copy_id])
@ -215,7 +219,7 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
@classmethod
@ndb.transactional()
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:
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.
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)
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()
def public_pem(self):
"""Returns: bytes"""
"""
Returns:
bytes:
"""
rsa = RSA.construct((base64_to_long(str(self.mod)),
base64_to_long(str(self.public_exponent))))
return rsa.exportKey(format='PEM')
def private_pem(self):
"""Returns: bytes"""
"""
Returns:
bytes:
"""
assert self.mod and self.public_exponent and self.private_exponent, str(self)
rsa = RSA.construct((base64_to_long(str(self.mod)),
base64_to_long(str(self.public_exponent)),
@ -354,7 +364,7 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
return rsa.exportKey(format='PEM')
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:
name = self.obj.as1.get('displayName')
if name:
@ -363,7 +373,7 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
return self.handle_or_id()
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.
@ -376,10 +386,10 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
"""Returns True if the given URL is this user's web URL (homepage).
Args:
url: str
url (str)
Returns:
boolean
bool:
"""
if not url:
return False
@ -399,7 +409,7 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
"""Returns this user's ActivityPub address, eg ``@me@foo.com``.
Returns:
str
str:
"""
# TODO: use self.handle_as? need it to fall back to id?
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):
"""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.
@ -435,7 +445,7 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
Defaults to this user's key id.
Returns:
str
str:
"""
return self.key.id()
@ -516,11 +526,10 @@ class Object(StringIdModel):
new = None
changed = None
"""
Protocol and subclasses set these in fetch if this Object is new or if its
contents have changed from what was originally loaded from the datastore.
If either one is None, that means we don't know whether this Object is
new/changed.
"""Protocol and subclasses set these in fetch if this :class:`Object` is
new or if its contents have changed from what was originally loaded from the
datastore. If either one is None, that means we don't know whether this
:class:`Object` is new/changed.
:attr:`changed` is populated by :meth:`Object.activity_changed()`.
"""
@ -664,12 +673,12 @@ class Object(StringIdModel):
@classmethod
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
to URL-encoding #s instead.
to URL-encoding ``#``s instead.
https://github.com/snarfed/bridgy-fed/issues/469
See "meth:`proxy_url()` for the inverse.
"""
return super().get_by_id(id.replace('^^', '#'))
@ -677,14 +686,14 @@ class Object(StringIdModel):
@classmethod
@ndb.transactional()
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.
Only populates non-False/empty property values in props into the object.
Also populates the :attr:`new` and :attr:`changed` properties.
If a matching :class:`Object` doesn't exist in the datastore, creates it
first. Only populates non-False/empty property values in props into the
object. Also populates the :attr:`new` and :attr:`changed` properties.
Returns:
:class:`Object`
Object:
"""
obj = cls.get_by_id(id)
if obj:
@ -723,8 +732,8 @@ class Object(StringIdModel):
Args:
fetch_blobs (bool): whether to fetch images and other blobs, store
them in :class:`arroba.AtpRemoteBlob'\s if they don't already exist,
and fill them into the returned object.
them in :class:`arroba.datastore_storage.AtpRemoteBlob`\s if they
don't already exist, and fill them into the returned object.
"""
if self.bsky:
return self.bsky
@ -744,14 +753,14 @@ class Object(StringIdModel):
return {}
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.
Used to populate :attr:`changed`.
Args:
other_as1: dict AS1 object, or none
other_as1 (dict): AS1 object, or none
"""
return (as1.activity_changed(self.as1, other_as1)
if self.as1 and other_as1
@ -760,10 +769,11 @@ class Object(StringIdModel):
def proxy_url(self):
"""Returns the Bridgy Fed proxy URL to render this post as HTML.
Note that some webmention receivers are struggling with the %23s
(URL-encoded #s) in these paths:
https://github.com/snarfed/bridgy-fed/issues/469
https://github.com/pfefferle/wordpress-webmention/issues/359
Note that some webmention receivers are struggling with the ``%23``s
(URL-encoded ``#``s) in these paths:
* https://github.com/snarfed/bridgy-fed/issues/469
* https://github.com/pfefferle/wordpress-webmention/issues/359
See "meth:`get_by_id()` for the inverse.
"""
@ -845,16 +855,17 @@ class Follower(ndb.Model):
@classmethod
@ndb.transactional()
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:
from_: :class:`User`
to: :class:`User`
from_ (User)
to (User)
Returns:
:class:`Follower`
Follower:
"""
assert from_
assert to
@ -878,11 +889,11 @@ class Follower(ndb.Model):
def fetch_page(collection):
"""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.
Args:
collection, str, 'followers' or 'following'
collection (str): ``followers`` or ``following``
Returns:
(followers, new_before, new_after) tuple with:
@ -913,22 +924,22 @@ class Follower(ndb.Model):
def fetch_page(query, model_class):
"""Fetches a page of results from a datastore query.
Uses the `before` and `after` query params (if provided; should be ISO8601
timestamps) and the queried model class's `updated` property to identify the
page to fetch.
Uses the ``before`` and ``after`` query params (if provided; should be
ISO8601 timestamps) and the queried model class's ``updated`` property to
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.
Args:
query: :class:`ndb.Query`
model_class: ndb model class
query (ndb.Query)
model_class (class)
Returns:
(results, new_before, new_after) tuple with:
results: list of query result entities
new_before, new_after: str query param values for `before` and `after`
to fetch the previous and next pages, respectively
(list of entities, str, str) tuple:
(results, new_before, new_after), where new_before and new_after are query
param values for ``before`` and ``after`` to fetch the previous and next
pages, respectively
"""
# if there's a paging param ('before' or 'after'), update query with it
# 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`.
Args:
protocol: str
id: str
protocol (str):
id (str):
Raises:
:class:`werkzeug.exceptions.HTTPException` on error or redirect
@ -231,19 +231,18 @@ def bridge_user():
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
entities for rendering in objects.html.
Wraps :func:`models.fetch_page` and adds attributes to the returned
:class:`models.Object` entities for rendering in ``objects.html``.
Args:
query: :class:`ndb.Query`
query (ndb.Query)
Returns:
(results, new_before, new_after) tuple with:
results: list of Object entities
new_before, new_after: str query param values for `before` and `after`
to fetch the previous and next pages, respectively
(list of models.Object, str, str) tuple:
(results, new ``before`` query param, new ``after`` query param)
to fetch the previous and next pages, respectively
"""
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.
Attributes:
LABEL: str, human-readable lower case name
OTHER_LABELS: sequence of str, label aliases
ABBREV: str, lower case abbreviation, used in URL paths
LABEL (str): human-readable lower case name
OTHER_LABELS (sequence): of str, label aliases
ABBREV (str): lower case abbreviation, used in URL paths
"""
ABBREV = None
OTHER_LABELS = ()
@ -74,13 +74,13 @@ class Protocol:
...based on the request's hostname.
Args:
fed (str or Protocol): protocol to return if the current request is on
``fed.brid.gy``
fed (str or protocol.Protocol): protocol to return if the current
request is on ``fed.brid.gy``
Returns:
Protocol subclass: ...or None if the provided domain or request
hostname domain is not a subdomain of ``brid.gy` or isn't a known
protocol
protocol.Protocol subclass: protocol, or None if the provided domain
or request hostname domain is not a subdomain of ``brid.gy` or isn't
a known protocol
"""
return Protocol.for_bridgy_subdomain(request.host, fed=fed)
@ -89,14 +89,13 @@ class Protocol:
"""Returns the protocol for a brid.gy subdomain.
Args:
domain_or_url: str
fed (str or Protocol): protocol to return if the current request is on
``fed.brid.gy``
domain_or_url (str)
fed (str or protocol.Protocol): protocol to return if the current
request is on ``fed.brid.gy``
Returns:
Protocol subclass: ...or None if the provided domain or request
hostname domain is not a subdomain of ``brid.gy` or isn't a known
protocol
Returns: protocol.Protocol subclass: protocol, or None if the provided
domain or request hostname domain is not a subdomain of ``brid.gy` or
isn't a known protocol
"""
domain = (util.domain_from_link(domain_or_url, minimize=False)
if util.is_web(domain_or_url)
@ -116,10 +115,10 @@ class Protocol:
'https://ap.brid.gy/foo/bar'.
Args:
path: str
path (str)
Returns:
str, URL
str: URL
"""
return urljoin(f'https://{cls.ABBREV or "fed"}{common.SUPERDOMAIN}/', path)
@ -196,7 +195,7 @@ class Protocol:
@classmethod
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.
@ -344,8 +343,8 @@ class Protocol:
default_g_user is True, otherwise None.
Args:
obj: :class:`Object`
default_g_user: boolean
obj (models.Object)
default_g_user (bool)
Returns:
:class:`ndb.Key` or None
@ -363,9 +362,9 @@ class Protocol:
To be implemented by subclasses.
Args:
obj: :class:`Object` with activity to send
url: str, destination URL to send to
log_data: boolean, whether to log full data object
obj (models.Object): with activity to send
url (str): destination URL to send to
log_data (bool): whether to log full data object
Returns:
True if the activity is sent successfully, False if it is ignored or
@ -389,7 +388,7 @@ class Protocol:
To be implemented by subclasses.
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.
**kwargs: subclass-specific
@ -413,7 +412,7 @@ class Protocol:
To be implemented by subclasses.
Args:
obj: :class:`Object`
obj (models.Object):
Returns:
(response body, dict with HTTP headers) tuple appropriate to be
@ -436,8 +435,8 @@ class Protocol:
inbox.
Args:
obj: :class:`Object`
shared: boolean, optional. If `True`, returns a common/shared
obj (models.Object):
shared (bool): optional. If `True`, returns a common/shared
endpoint, eg ActivityPub's `sharedInbox`, that can be reused for
multiple recipients for efficiency
@ -453,9 +452,9 @@ class Protocol:
Default implementation here, subclasses may override.
Args:
url: str
url (str):
Returns: boolean
Returns: bool
"""
return util.domain_or_parent_in(util.domain_from_link(url),
DOMAIN_BLOCKLIST + DOMAINS)
@ -468,7 +467,7 @@ class Protocol:
raises :class:`werkzeug.exceptions.BadRequest`.
Args:
obj: :class:`Object`
obj (models.Object):
Returns:
(response body, HTTP status code) tuple for Flask response
@ -639,7 +638,7 @@ class Protocol:
"""Handles an incoming follow activity.
Args:
obj: :class:`Object`, follow activity
obj (models.Object): follow activity
"""
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.
Args:
obj: :class:`Object`
obj (models.Object)
Returns:
obj: :class:`Object`, the same one if the input obj is an activity,
otherwise a new one
Object: ``obj`` if it's an activity, otherwise a new object
"""
if obj.type not in ('note', 'article', 'comment'):
return obj
@ -795,7 +793,7 @@ class Protocol:
"""Delivers an activity to its external recipients.
Args:
obj: :class:`Object`, activity to deliver
obj (models.Object): activity to deliver
"""
# find delivery targets
# 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.
Args:
obj (:class:`models.Object`)
obj (models.Object)
Returns:
dict: {
:class:`Target`: original (in response to) :class:`models.Object`,
if any, otherwise None
}
dict: maps :class:`Target`: to original (in response to)
:class:`models.Object`, if any, otherwise None
"""
logger.info('Finding recipients and their targets')
@ -1000,24 +996,25 @@ class Protocol:
Note that :meth:`Object._post_put_hook` updates the cache.
Args:
id: str
remote: boolean, whether to fetch the object over the network. If True,
id (str)
remote (bool): whether to fetch the object over the network. If True,
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
None. Default (None) means to fetch over the network only if we
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
datastore after a successful remote 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
* 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:
:class:`requests.HTTPError`, anything else that :meth:`fetch` raises
requests.HTTPError: anything that :meth:`fetch` raises
"""
assert local or remote is not False
@ -1081,19 +1078,19 @@ class Protocol:
@app.post('/queue/receive')
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
* user: urlsafe :class:`ndb.Key` of the :class:`User` this activity is on
behalf of. This user will be loaded into `g.user`.
TODO: migrate incoming webmentions and AP inbox deliveries to this.
difficulty is that parts of Protocol.receive depend on setup in
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.
TODO: migrate incoming webmentions and AP inbox deliveries to this. The
difficulty is that parts of :meth:`protocol.Protocol.receive` depend on
setup in :func:`web.webmention` and :func:`activitypub.inbox`, eg
:class:`models.Object` with ``new`` and ``changed``, ``g.user`` (which
:meth:`receive` now loads), HTTP request details, etc. See stash for attempt
at this for :class:`web.Web`.
"""
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.
Only for Web users. Other protocols (including Web sometimes) use /convert/ in
convert.py instead.
Only for :class:`web.Web` users. Other protocols (including :class:`web.Web`
sometimes) use ``/`` convert ``/`` in convert.py instead.
Serves /r/https://foo.com/bar URL paths, where https://foo.com/bar is a original
post for a Web user. Needed for Mastodon interop, they require that AS2 object
ids and urls are on the same domain that serves them. Background:
Serves ``/r/https://foo.com/bar`` URL paths, where ``https://foo.com/bar`` is a
original post for a :class:`Web` user. Needed for Mastodon interop, they require
that AS2 object ids and urls are on the same domain that serves them.
Background:
https://github.com/snarfed/bridgy-fed/issues/16#issuecomment-424799599
https://github.com/tootsuite/mastodon/pull/6219#issuecomment-429142747
* https://github.com/snarfed/bridgy-fed/issues/16#issuecomment-424799599
* https://github.com/tootsuite/mastodon/pull/6219#issuecomment-429142747
The conneg makes these /r/ URLs searchable in Mastodon:
The conneg makes these ``/r/`` URLs searchable in Mastodon:
https://github.com/snarfed/bridgy-fed/issues/352
"""
import logging
import re
@ -47,9 +47,9 @@ DOMAIN_ALLOWLIST = frozenset((
def redir(to):
"""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
it's requested with AS2 conneg in the Accept header, fetches and converts
and serves it as AS2.
E.g. redirects ``/r/https://foo.com/bar?baz`` to
``https://foo.com/bar?baz``, or if it's requested with AS2 conneg in the
``Accept`` header, fetches and converts and serves it as AS2.
"""
if request.args:
to += '?' + urllib.parse.urlencode(request.args)

Wyświetl plik

@ -1,4 +1,3 @@
# coding=utf-8
"""Unit tests for activitypub.py."""
from base64 import b64encode
import copy

Wyświetl plik

@ -1,4 +1,3 @@
# coding=utf-8
"""Unit tests for models.py."""
from unittest.mock import patch

Wyświetl plik

@ -1,4 +1,3 @@
# coding=utf-8
"""Unit tests for webmention.py."""
import copy
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 difflib
import logging
@ -109,7 +109,7 @@ class Web(User, Protocol):
profile_id = web_url
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.
"""
@ -147,7 +147,8 @@ class Web(User, Protocol):
Uses stored representative h-card if available, falls back to id.
Returns: str
Returns:
str:
"""
id = self.key.id()
@ -171,9 +172,9 @@ class Web(User, Protocol):
"""Fetches site a couple ways to check for redirects and h-card.
Returns: :class:`Web` that was verified. May be different than
self! eg if self's domain started with www and we switch to the root
domain.
Returns:
web.Web: user that was verified. May be different than self! eg if
self 's domain started with www and we switch to the root domain.
"""
domain = self.key.id()
logger.info(f'Verifying {domain}')
@ -333,16 +334,17 @@ class Web(User, Protocol):
def fetch(cls, obj, gateway=False, check_backlink=False, **kwargs):
"""Fetches a URL over HTTP and extracts its microformats2.
Follows redirects, but doesn't change the original URL in obj's id! The
:class:`Model` class doesn't allow that anyway, but more importantly, we
want to preserve that original URL becase other objects may refer to it
instead of the final redirect destination URL.
Follows redirects, but doesn't change the original URL in ``obj``'s id!
The :class:`Model` class doesn't allow that anyway, but more
importantly, we want to preserve that original URL becase other objects
may refer to it instead of the final redirect destination URL.
See :meth:`Protocol.fetch` for other background.
Args:
gateway: passed through to :func:`webutil.util.fetch_mf2`
check_backlink: bool, optional, whether to require a link to Bridgy
gateway (bool): passed through to
: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.
kwargs: ignored
"""

Wyświetl plik

@ -157,7 +157,7 @@ class Webfinger(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.
https://tools.ietf.org/html/rfc6415#section-3
@ -173,7 +173,7 @@ class HostMeta(flask_util.XrdOrJrd):
@app.get('/.well-known/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()),
{'Content-Type': 'application/xrds+xml'})
@ -187,8 +187,8 @@ def fetch(addr):
returning None
Args:
addr (str): a Webfinger-compatible address, eg @x@y, acct:x@y, or
https://x/y
addr (str): a Webfinger-compatible address, eg ``@x@y``, ``acct:x@y``, or
``https://x/y``
Returns:
dict: fetched WebFinger data, or None on error