kopia lustrzana https://github.com/snarfed/bridgy-fed
docs: fix docstring formatting, other tweaks
rodzic
6442acb244
commit
db29ad7757
|
@ -67,7 +67,7 @@ class ActivityPub(User, Protocol):
|
|||
"""Validate id, require URL, don't allow Bridgy Fed domains.
|
||||
|
||||
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
|
||||
|
|
25
atproto.py
25
atproto.py
|
@ -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
|
||||
"""
|
||||
|
|
55
common.py
55
common.py
|
@ -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}'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
17
docs/conf.py
17
docs/conf.py
|
@ -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 ------------------------------------------------------------
|
||||
|
|
|
@ -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__
|
||||
|
|
|
@ -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
8
ids.py
|
@ -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
145
models.py
|
@ -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
|
||||
|
|
19
pages.py
19
pages.py
|
@ -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)
|
||||
|
||||
|
|
113
protocol.py
113
protocol.py
|
@ -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())}')
|
||||
|
||||
|
|
24
redirect.py
24
redirect.py
|
@ -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)
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# coding=utf-8
|
||||
"""Unit tests for activitypub.py."""
|
||||
from base64 import b64encode
|
||||
import copy
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# coding=utf-8
|
||||
"""Unit tests for models.py."""
|
||||
from unittest.mock import patch
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# coding=utf-8
|
||||
"""Unit tests for webmention.py."""
|
||||
import copy
|
||||
from unittest.mock import patch
|
||||
|
|
26
web.py
26
web.py
|
@ -1,4 +1,4 @@
|
|||
"""Handles inbound webmentions."""
|
||||
"""Webmention protocol with microformats2 in HTML, aka the IndieWeb stack."""
|
||||
import datetime
|
||||
import 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
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
Ładowanie…
Reference in New Issue