Bridgy Fed
Reference documentation.
activitypub
ActivityPub protocol implementation.
- class activitypub.ActivityPub(**kwargs)[source]
-
ActivityPub protocol class.
Key id is AP/AS2 actor id URL. (Not fediverse/WebFinger @-@ handle!)
- classmethod owns_id(id)[source]
Returns None if
id
is an http(s) URL, False otherwise.All AP ids are http(s) URLs, but not all http(s) URLs are AP ids.
https://www.w3.org/TR/activitypub/#obj-id
I used to include a heuristic here that no actor is the root path on its host, which was nice because it let us assume that home pages are Web users without making any network requests…but then I inevitably ran into AP actors that _are_ the root path, eg microblog.pub sites like https://bw3.dev/ .
- classmethod owns_handle(handle, allow_internal=False)[source]
Returns True if handle is a WebFinger
@-@
handle, False otherwise.Example:
@user@instance.com
. The leading@
is optional.https://datatracker.ietf.org/doc/html/rfc7033#section-3.1 https://datatracker.ietf.org/doc/html/rfc7033#section-4.5
- classmethod target_for(obj, shared=False)[source]
Returns
obj
’s or its author’s/actor’s inbox, if available.
- classmethod send(obj, url, from_user=None, orig_obj=None)[source]
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 intocc
.
- classmethod fetch(obj, **kwargs)[source]
Tries to fetch an AS2 object.
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
Uses HTTP content negotiation via the
Content-Type
header. If the url is HTML and it has arel-alternate
link with an AS2 content type, fetches and returns that URL.Includes an HTTP Signature with the request.
Mastodon requires this signature if
AUTHORIZED_FETCH
aka secure mode is on: https://docs.joinmastodon.org/admin/config/#authorized_fetchSigns the request with the current user’s key. If not provided, defaults to using @snarfed.org@snarfed.org’s key.
See
protocol.Protocol.fetch()
for more details.- Parameters:
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, False otherwise
- Return type:
- Raises:
werkzeug.exceptions.HTTPException – will have an additional
requests_response
attribute with the lastrequests.Response
we received.
- activitypub.signed_request(fn, url, data=None, headers=None, from_user=None, **kwargs)[source]
Wraps
requests.*
and adds HTTP Signature.- Parameters:
fn (callable) –
util.requests_get()
orutil.requests_post()
url (str)
data (dict) – optional AS2 object
from_user (models.User) – user to sign request as; optional. If not provided, uses the default user
@snarfed.org@snarfed.org
.kwargs – passed through to requests
- Return type:
- activitypub.postprocess_as2(activity, orig_obj=None, wrap=True)[source]
Prepare an AS2 object to be served or sent via ActivityPub.
- activitypub.postprocess_as2_actor(actor, user)[source]
Prepare an AS2 actor object to be served or sent via ActivityPub.
Modifies actor in place.
- Parameters:
actor (dict) – AS2 actor object
user (models.User) – current user
- Returns:
actor dict
atproto
ATProto protocol implementation.
- atproto.did_to_handle(did)[source]
Resolves a DID to a handle _if_ we have the DID doc stored locally.
- class atproto.Cursor(**kwargs)[source]
Bases:
StringIdModel
The last cursor (sequence number) we’ve seen for a host and event stream.
https://atproto.com/specs/event-stream#sequence-numbers
Key id is
[HOST] [XRPC]
, where[XRPC]
is the NSID of the XRPC method for the event stream. For example, subscribeRepos on the production relay isbsky.network com.atproto.sync.subscribeRepos
.cursor
is the latest sequence number that we know we’ve seen, so when we re-subscribe to this event stream, we should sendcursor + 1
.
- class atproto.ATProto(**kwargs)[source]
-
AT Protocol class.
Key id is DID, currently either did:plc or did:web. https://atproto.com/specs/did
- web_url()[source]
Returns this user’s web URL (homepage), eg
https://foo.com/
.To be implemented by subclasses.
- Returns:
str
- classmethod owns_id(id)[source]
Returns whether this protocol owns the id, or None if it’s unclear.
To be implemented by subclasses.
IDs are string identities that uniquely identify users, and are intended primarily to be machine readable and usable. Compare to handles, which are human-chosen, human-meaningful, and often but not always unique.
Some protocols’ ids are more or less deterministic based on the id format, eg AT Protocol owns
at://
URIs. Others, like http(s) URLs, could be owned by eg Web or ActivityPub.This should be a quick guess without expensive side effects, eg no external HTTP fetches to fetch the id itself or otherwise perform discovery.
Returns False if the id’s domain is in
common.DOMAIN_BLOCKLIST
.
- classmethod owns_handle(handle, allow_internal=False)[source]
Returns whether this protocol owns the handle, or None if it’s unclear.
To be implemented by subclasses.
Handles are string identities that are human-chosen, human-meaningful, and often but not always unique. Compare to IDs, which uniquely identify users, and are intended primarily to be machine readable and usable.
Some protocols’ handles are more or less deterministic based on the id format, eg ActivityPub (technically WebFinger) handles are
@user@instance.com
. Others, like domains, could be owned by eg Web, ActivityPub, AT Protocol, or others.This should be a quick guess without expensive side effects, eg no external HTTP fetches to fetch the id itself or otherwise perform discovery.
- classmethod handle_to_id(handle)[source]
Converts a handle to an id.
To be implemented by subclasses.
May incur network requests, eg DNS queries or HTTP requests.
- profile_id()[source]
Returns the id of this user’s profile object in its native protocol.
Examples:
Web: home page URL, eg
https://me.com/
ActivityPub: actor URL, eg
https://instance.com/users/me
ATProto: profile AT URI, eg
at://did:plc:123/app.bsky.actor.profile/self
Defaults to this user’s key id.
- Return type:
- classmethod bridged_web_url_for(user)[source]
Returns a bridged user’s profile URL on bsky.app.
For example, returns
https://bsky.app/profile/alice.com.web.brid.gy
for Web useralice.com
.- Parameters:
user (models.User)
- Returns:
str, or None if there isn’t a canonical URL
- classmethod target_for(obj, shared=False)[source]
Returns our PDS URL as the target for the given object.
ATProto delivery is indirect. We write all records to the user’s local repo that we host, then relays and other subscribers receive them via the subscribeRepos event streams. So, we use a single target, our base URL (eg
https://atproto.brid.gy
) as the PDS URL, for all activities.
- is_blocklisted(allow_internal=False)[source]
Returns True if we block the given URL and shouldn’t deliver to it.
Default implementation here, subclasses may override.
- Parameters:
Returns: bool:
- classmethod create_for(user)[source]
Creates an ATProto repo and profile for a non-ATProto user.
- Parameters:
user (models.User)
- Raises:
ValueError – if the user’s handle is invalid, eg begins or ends with an underscore or dash
- classmethod send(obj, url, from_user=None, orig_obj=None)[source]
Creates a record if we own its repo.
Creates the repo first if it doesn’t exist.
If the repo’s DID doc doesn’t say we’re its PDS, does nothing and returns False.
Doesn’t deliver anywhere externally! Relays will receive this record through
subscribeRepos
and then deliver it to AppView(s), which will notify recipients as necessary.
- classmethod load(id, did_doc=False, **kwargs)[source]
Thin wrapper that converts DIDs and bsky.app URLs to at:// URIs.
- Parameters:
did_doc (bool) – if True, loads and returns a DID document object instead of an
app.bsky.actor.profile/self
.
- classmethod fetch(obj, **kwargs)[source]
Tries to fetch a ATProto object.
- Parameters:
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, False otherwise
- Return type:
- classmethod create_report(input, from_user)[source]
Sends a
createReport
for aflag
activity.- Parameters:
input (dict) –
createReport
inputfrom_user (models.User) – user (actor) this flag is from
- Returns:
- True if the report was sent successfully, False if the flag’s
actor is not bridged into ATProto
- Return type:
- atproto.poll_notifications()[source]
Fetches and enqueueus new activities from the AppView for our users.
Uses the
listNotifications
endpoint, which is intended for end users. 🤷https://github.com/bluesky-social/atproto/discussions/1538
TODO: unify with poll_posts
common
Misc common utilities.
- common.base64_to_long(x)[source]
Converts from URL safe base64 encoding to long integer.
Originally from
django_salmon.magicsigs
. Used inUser.public_pem()
andUser.private_pem()
.
- common.long_to_base64(x)[source]
Converts from long integer to base64 URL safe encoding.
Originally from
django_salmon.magicsigs
. Used inUser.get_or_create()
.
- common.error(err, status=400, exc_info=None, **kwargs)[source]
Like
oauth_dropins.webutil.flask_util.error()
, but wraps body in JSON.
- common.pretty_link(url, text=None, user=None, **kwargs)[source]
Wrapper around
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.- Parameters:
url (str)
text (str)
user (models.User) – current user
kwargs – passed through to
oauth_dropins.webutil.util.pretty_link()
- common.content_type(resp)[source]
Returns a
requests.Response
’s Content-Type, without charset suffix.
- common.redirect_wrap(url)[source]
Returns a URL on our domain that redirects to this URL.
…to satisfy Mastodon’s non-standard domain matching requirement. :(
- Parameters:
url (str)
https://github.com/snarfed/bridgy-fed/issues/16#issuecomment-424799599
https://github.com/tootsuite/mastodon/pull/6219#issuecomment-429142747
- Returns:
redirect url
- Return type:
- common.subdomain_wrap(proto, path=None)[source]
Returns the URL for a given path on this protocol’s subdomain.
Eg for the path
foo/bar
on ActivityPub, returnshttps://ap.brid.gy/foo/bar
.- Parameters:
proto (subclass of
protocol.Protocol
)- Returns:
URL
- Return type:
- common.unwrap(val, field=None)[source]
Removes our subdomain/redirect wrapping from a URL, if it’s there.
val
may be a string, dict, or list. dicts and lists are unwrapped recursively.Strings that aren’t wrapped URLs are left unchanged.
- common.webmention_endpoint_cache_key(url)[source]
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/701Example:
snarfed.org /
https://github.com/snarfed/bridgy-fed/issues/423
Adapted from
bridgy/util.py
.
- common.webmention_discover(url, **kwargs)[source]
Thin caching wrapper around
oauth_dropins.webutil.webmention.discover()
.
- common.add(seq, val)[source]
Appends
val
toseq
if seq doesn’t already contain it.Useful for treating repeated ndb properties like sets instead of lists.
- common.remove(seq, val)[source]
Removes
val
toseq
if seq contains it.Useful for treating repeated ndb properties like sets instead of lists.
- common.create_task(queue, delay=None, **params)[source]
Adds a Cloud Tasks task.
If running in a local server, runs the task handler inline instead of creating a task.
- Parameters:
queue (str) – queue name
delay (
datetime.timedelta
) – optional, used as task ETA (from now)params – form-encoded and included in the task request body
- Returns:
response from either running the task inline, if running in a local server, or the response from creating the task.
- Return type:
flask.Response or (str, int)
- common.report_exception(**kwargs)[source]
Reports the current exception to StackDriver Error Reporting.
https://cloud.google.com/error-reporting/docs/reference/libraries#client-libraries-install-python
If
DEBUG
isTrue
, re-raises the exception instead.Duplicated in
bridgy.util
.
convert
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 protocol.Protocol
subclasses.
- convert.convert(dest, _, src=None)[source]
Converts data from one protocol to another and serves it.
Fetches the source data if it’s not already stored.
- Parameters:
dest (str) – protocol
src (str) – protocol, only used when called by
convert_source_path_redirect()
follow
Remote follow handler.
- class follow.FollowStart(to_path, scopes=None)[source]
Bases:
Start
Starts the IndieAuth flow to add a follower to an existing user.
- class follow.FollowCallback(to_path, scopes=None)[source]
Bases:
Callback
IndieAuth callback to add a follower to an existing user.
- class follow.UnfollowStart(to_path, scopes=None)[source]
Bases:
Start
Starts the IndieAuth flow to remove a follower from an existing user.
- class follow.UnfollowCallback(to_path, scopes=None)[source]
Bases:
Callback
IndieAuth callback to remove a follower.
models
Datastore model classes.
- class models.Target(**kwargs)[source]
Bases:
Model
protocol.Protocol
+ URI pairs for identifying objects.These are currently used for:
delivery destinations, eg ActivityPub inboxes, webmention targets, etc.
copies of
Object
s andUser
s elsewhere, egat://
URIs for ATProto records, nevent etc bech32-encoded Nostr ids, ATProto user DIDs, etc.
Used in
google.cloud.ndb.model.StructuredProperty
s insideObject
andUser
; 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.For repeated StructuredPropertys, the hoisted properties are all repeated on the parent entity, and reconstructed into StructuredPropertys based on their order.
https://googleapis.dev/python/python-ndb/latest/model.html#google.cloud.ndb.model.StructuredProperty
- class models.ProtocolUserMeta(name, bases, class_dict)[source]
Bases:
MetaModel
User
metaclass. Registers all subclasses in thePROTOCOLS
global.
- models.reset_protocol_properties()[source]
Recreates various protocol properties to include choices from
PROTOCOLS
.
- class models.User(**kwargs)[source]
Bases:
StringIdModel
Abstract base class for a Bridgy Fed user.
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: https://tools.ietf.org/html/draft-cavage-http-signatures-12Not K-256 signing or rotation keys for AT Protocol, those are stored in
arroba.datastore_storage.AtpRepo
entities
- __init__(**kwargs)[source]
Constructor.
Sets
obj
explicitly because howevergoogle.cloud.ndb.model.Model
sets it doesn’t work with@property
and@obj.setter
below.
- classmethod get_by_id(id, allow_opt_out=False)[source]
Override to follow
use_instead
property and ``opt-out` status.Returns None if the user is opted out.
- classmethod get_or_create(id, propagate=False, allow_opt_out=False, **kwargs)[source]
Loads and returns a
User
. Creates it if necessary.
- property obj
Convenience accessor that loads
obj_key
from the datastore.
- classmethod load_multi(users)[source]
Loads
obj
for multiple users in parallel.- Parameters:
users (sequence of User)
- is_enabled(to_proto, explicit=False)[source]
Returns True if this user can be bridged to a given protocol.
Reasons this might return False: * We haven’t turned on bridging these two protocols yet. * The user is opted out or blocked. * The user is on a domain that’s opted out or blocked. * The from protocol requires opt in, and the user hasn’t opted in. *
explicit
is True, and this protocol supportsto_proto
bydefault, but the user hasn’t explicitly opted into it.
- enable_protocol(to_proto)[source]
Adds ``to_proto` to
enabled_protocols
.- Parameters:
to_proto (
protocol.Protocol
subclass)
- disable_protocol(to_proto)[source]
Removes ``to_proto` from
enabled_protocols
.- Parameters:
to_proto (
protocol.Protocol
subclass)
- web_url()[source]
Returns this user’s web URL (homepage), eg
https://foo.com/
.To be implemented by subclasses.
- Returns:
str
- is_web_url(url, ignore_www=False)[source]
Returns True if the given URL is this user’s web URL (homepage).
- profile_id()[source]
Returns the id of this user’s profile object in its native protocol.
Examples:
Web: home page URL, eg
https://me.com/
ActivityPub: actor URL, eg
https://instance.com/users/me
ATProto: profile AT URI, eg
at://did:plc:123/app.bsky.actor.profile/self
Defaults to this user’s key id.
- Return type:
- get_copy(proto)[source]
Returns the id for the copy of this user in a given protocol.
…or None if no such copy exists. If
proto
is this user, returns this user’s key id.- Parameters:
proto –
Protocol
subclass- Return type:
- user_link(handle=False, maybe_internal_link=True)[source]
Returns a pretty link to the user with name and profile picture.
If they’re opted in, links to their Bridgy Fed user page. Otherwise, links to their external account.
TODO: unify with
Object.actor_link()
?
- class models.Object(*args, **kwargs)[source]
Bases:
StringIdModel
An activity or other object, eg actor.
Key name is the id. We synthesize ids if necessary.
- 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 thisObject
is new/changed.changed
is populated byactivity_changed()
.
- classmethod get_by_id(id)[source]
Override
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. https://github.com/snarfed/bridgy-fed/issues/469
- classmethod get_or_create(id, actor=None, **props)[source]
Returns an
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 thenew
andchanged
properties.
- add(prop, val)[source]
Adds a value to a multiply-valued property. Uses
self.lock
.- Parameters:
prop (str)
val
- remove(prop, val)[source]
Removes a value from a multiply-valued property. Uses
self.lock
.- Parameters:
prop (str)
val
- activity_changed(other_as1)[source]
Returns True if this activity is meaningfully changed from
other_as1
.…otherwise False.
Used to populate
changed
.- Parameters:
other_as1 (dict) – AS1 object, or none
- actor_link(image=True, sized=False, user=None)[source]
Returns a pretty HTML link with the actor’s name and picture.
TODO: unify with
User.user_link()
?
- get_copy(proto)[source]
Returns the id for the copy of this object in a given protocol.
…or None if no such copy exists. If
proto
issource_protocol
, returns this object’s key id.- Parameters:
proto –
Protocol
subclass- Return type:
- resolve_ids()[source]
Resolves “copy” ids, subdomain ids, etc with their originals.
The end result is that all ids are original “source” ids, ie in the protocol that they first came from.
Specifically, resolves:
ids in
User.copies
andObject.copies
, eg ATProto records and Nostr events that we bridged, to the ids of their original objects in their source protocol, egat://did:plc:abc/app.bsky.feed.post/123
=>https://mas.to/@user/456
.Bridgy Fed subdomain URLs to the ids embedded inside them, eg
https://bsky.brid.gy/ap/did:plc:xyz
=>did:plc:xyz
ATProto bsky.app URLs to their DIDs or at:// URIs, eg
https://bsky.app/profile/a.com
=>did:plc:123
…in these AS1 fields, in place:
id
actor
author
object
object.actor
object.author
object.id
object.inReplyTo
tags.[objectType=mention].url
protocol.Protocol.translate_ids()
is partly the inverse of this. Much of the same logic is duplicated there!TODO: unify with
normalize_ids()
,Object.normalize_ids()
.
- normalize_ids()[source]
Normalizes ids to their protocol’s canonical representation, if any.
For example, normalizes ATProto
https://bsky.app/...
URLs to DIDs for profiles,at://
URIs for posts.Modifies this object in place.
TODO: unify with
resolve_ids()
,Protocol.translate_ids()
.
- class models.Follower(**kwargs)[source]
Bases:
Model
A follower of a Bridgy Fed user.
- classmethod get_or_create(*, from_, to, **kwargs)[source]
Returns a Follower with the given
from_
andto
users.If a matching
Follower
doesn’t exist in the datastore, creates it first.
- static fetch_page(collection, user)[source]
Fetches a page of :class:`Follower`s for a given user.
Wraps
fetch_page()
. Paging uses thebefore
andafter
query parameters, if available in the request.- Parameters:
- Returns:
results, annotated with an extra
user
attribute that holds the follower or followingUser
, and new str query param values forbefore
andafter
to fetch the previous and next pages, respectively- Return type:
- models.fetch_objects(query, by=None, user=None)[source]
Fetches a page of
Object
entities from a datastore query.Wraps
fetch_page()
and adds attributes to the returnedObject
entities for rendering inobjects.html
.
- models.fetch_page(query, model_class, by=None)[source]
Fetches a page of results from a datastore query.
Uses the
before
andafter
query params (if provided; should be ISO8601 timestamps) and theby
property to identify the page to fetch.Populates a
log_url_path
property on each result entity that points to a its most recent logged request.- Parameters:
query (google.cloud.ndb.query.Query)
model_class (class)
by (ndb.model.Property) – paging property, eg
Object.updated
orObject.created
- Returns:
(results, new_before, new_after), where new_before and new_after are query param values for
before
andafter
to fetch the previous and next pages, respectively- Return type:
- models.get_original(copy_id, keys_only=None)[source]
Fetches a user or object with a given id in copies.
Thin wrapper around
get_copies()
that returns the first matching result.Also see :Object:`get_copy` and :User:`get_copy`.
- models.get_originals(copy_ids, keys_only=None)[source]
Fetches users (across all protocols) for a given set of copies.
Also see :Object:`get_copy` and :User:`get_copy`.
pages
UI pages.
- pages.load_user(protocol, id)[source]
Loads and returns the current request’s user.
- Parameters:
- Return type:
- Raises:
- pages.serve_feed(*, objects, format, user, title, as_snippets=False, quiet=False)[source]
Generates a feed based on :class:`Object`s.
- Parameters:
objects (sequence of models.Object)
format (str) –
html
,atom
, orrss
user (models.User)
title (str)
as_snippets (bool) – if True, render short snippets for objects instead of full contents
quiet (bool) – if True, exclude follows, unfollows, likes, and reposts
- Returns:
Flask response
- Return type:
protocol
Base protocol class and common code.
- class protocol.Protocol[source]
Bases:
object
Base protocol class. Not to be instantiated; classmethods only.
- PHRASE
human-readable name or phrase. Used in phrases like
Follow this person on {PHRASE}
- Type:
- CONTENT_TYPE
MIME type of this protocol’s native data format, appropriate for the
Content-Type
HTTP header.- Type:
- HAS_FOLLOW_ACCEPTS
whether this protocol supports explicit accept/reject activities in response to follows, eg ActivityPub
- Type:
- HAS_COPIES
whether this protocol is push and needs us to proactively create “copy” users and objects, as opposed to pulling converted objects on demand
- Type:
- REQUIRES_AVATAR
whether accounts on this protocol are required to have a profile picture. If they don’t, their
User.status
will beblocked
.- Type:
- REQUIRES_NAME
whether accounts on this protocol are required to have a profile name that’s different than their handle or id. If they don’t, their
User.status
will beblocked
.- Type:
- REQUIRES_OLD_ACCOUNT
(bool): whether accounts on this protocol are required to be at least
common.OLD_ACCOUNT_AGE
old. If their profile includes creation date and it’s not old enough, theirUser.status
will beblocked
.
- DEFAULT_ENABLED_PROTOCOLS
labels of other protocols that are automatically enabled for this protocol to bridge into
- static for_request(fed=None)[source]
Returns the protocol for the current request.
…based on the request’s hostname.
- Parameters:
fed (str or protocol.Protocol) – protocol to return if the current request is on
fed.brid.gy
- Returns:
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 type:
- static for_bridgy_subdomain(domain_or_url, fed=None)[source]
Returns the protocol for a brid.gy subdomain.
- Parameters:
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 ofbrid.gy
or isn’t a known protocol- Return type:
class
- classmethod owns_id(id)[source]
Returns whether this protocol owns the id, or None if it’s unclear.
To be implemented by subclasses.
IDs are string identities that uniquely identify users, and are intended primarily to be machine readable and usable. Compare to handles, which are human-chosen, human-meaningful, and often but not always unique.
Some protocols’ ids are more or less deterministic based on the id format, eg AT Protocol owns
at://
URIs. Others, like http(s) URLs, could be owned by eg Web or ActivityPub.This should be a quick guess without expensive side effects, eg no external HTTP fetches to fetch the id itself or otherwise perform discovery.
Returns False if the id’s domain is in
common.DOMAIN_BLOCKLIST
.
- classmethod owns_handle(handle, allow_internal=False)[source]
Returns whether this protocol owns the handle, or None if it’s unclear.
To be implemented by subclasses.
Handles are string identities that are human-chosen, human-meaningful, and often but not always unique. Compare to IDs, which uniquely identify users, and are intended primarily to be machine readable and usable.
Some protocols’ handles are more or less deterministic based on the id format, eg ActivityPub (technically WebFinger) handles are
@user@instance.com
. Others, like domains, could be owned by eg Web, ActivityPub, AT Protocol, or others.This should be a quick guess without expensive side effects, eg no external HTTP fetches to fetch the id itself or otherwise perform discovery.
- classmethod handle_to_id(handle)[source]
Converts a handle to an id.
To be implemented by subclasses.
May incur network requests, eg DNS queries or HTTP requests.
- classmethod key_for(id)[source]
Returns the
google.cloud.ndb.Key
for a given id’smodels.User
.To be implemented by subclasses. Canonicalizes the id if necessary.
If called via Protocol.key_for, infers the appropriate protocol with
for_id()
. If called with a concrete subclass, uses that subclass as is.- Returns:
matching key, or None if the given id is not a valid
User
id for this protocol.- Return type:
google.cloud.ndb.Key
- static for_handle(handle)[source]
Returns the protocol for a given handle.
May incur expensive side effects like resolving the handle itself over the network or other discovery.
- classmethod bridged_web_url_for(user)[source]
Returns the web URL for a user’s bridged profile in this protocol.
For example, for Web user
alice.com
,ATProto.bridged_web_url_for()
returnshttps://bsky.app/profile/alice.com.web.brid.gy
- Parameters:
user (models.User)
- Returns:
str, or None if there isn’t a canonical URL
- classmethod actor_key(obj)[source]
Returns the
User
: key for a given object’s author or actor.- Parameters:
obj (models.Object)
- Return type:
google.cloud.ndb.key.Key or None
- classmethod bot_user_id()[source]
Returns the Web user id for the bot user for this protocol.
For example,
'bsky.brid.gy'
for ATProto.- Return type:
- classmethod create_for(user)[source]
Creates a copy user in this protocol.
Should add the copy user to
copies
.- Parameters:
user (models.User) – original source user. Shouldn’t already have a copy user for this protocol in
copies
.- Raises:
ValueError – if we can’t create a copy of the given user in this protocol
- classmethod send(obj, url, from_user=None, orig_obj=None)[source]
Sends an outgoing activity.
To be implemented by subclasses.
- Parameters:
obj (models.Object) – with activity to send
url (str) – destination URL to send to
from_user (models.User) – user (actor) this activity is from
orig_obj (models.Object) – the “original object” that this object refers to, eg replies to or reposts or likes
- Returns:
True if the activity is sent successfully, False if it is ignored or otherwise unsent due to protocol logic, eg no webmention endpoint, protocol doesn’t support the activity type. (Failures are raised as exceptions.)
- Return type:
- Raises:
werkzeug.HTTPException if the request fails –
- classmethod fetch(obj, **kwargs)[source]
Fetches a protocol-specific object and populates it in an
Object
.Errors are raised as exceptions. If this method returns False, the fetch didn’t fail but didn’t succeed either, eg the id isn’t valid for this protocol, or the fetch didn’t return valid data for this protocol.
To be implemented by subclasses.
- Parameters:
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
- Returns:
True if the object was fetched and populated successfully, False otherwise
- Return type:
- Raises:
werkzeug.HTTPException – if the fetch fails
- classmethod convert(obj, from_user=None, **kwargs)[source]
Converts an
Object
to this protocol’s data format.For example, an HTML string for
Web
, or a dict with AS2 JSON andapplication/activity+json
forActivityPub
.Just passes through to
_convert()
, then does minor protocol-independent postprocessing.- Parameters:
obj (models.Object)
from_user (models.User) – user (actor) this activity/object is from
kwargs – protocol-specific, passed through to
_convert()
- Returns:
converted object in the protocol’s native format, often a dict
- classmethod target_for(obj, shared=False)[source]
Returns an
Object
’s delivery target (endpoint).To be implemented by subclasses.
Examples:
If obj has
source_protocol
web
, returns its URL, as a webmention target.If obj is an
activitypub
actor, returns its inbox.If obj is an
activitypub
object, returns it’s author’s or actor’s inbox.
- Parameters:
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
- Returns:
target endpoint, or None if not available.
- Return type:
- classmethod is_blocklisted(url, allow_internal=False)[source]
Returns True if we block the given URL and shouldn’t deliver to it.
Default implementation here, subclasses may override.
- Parameters:
Returns: bool:
- classmethod translate_ids(obj)[source]
Translates all ids in an AS1 object to a specific protocol.
Infers source protocol for each id value separately.
For example, if
proto
isActivityPub
, the ATProto URIat://did:plc:abc/coll/123
will be converted tohttps://bsky.brid.gy/ap/at://did:plc:abc/coll/123
.Wraps these AS1 fields:
id
actor
author
object
object.actor
object.author
object.id
object.inReplyTo
tags.[objectType=mention].url
This is the inverse of
models.Object.resolve_ids()
. Much of the same logic is duplicated there!TODO: unify with
Object.resolve_ids()
,models.Object.normalize_ids()
.- Parameters:
to_proto (Protocol subclass)
obj (dict) – AS1 object or activity (not
models.Object
!)
- Returns:
wrapped version of
obj
- Return type:
- classmethod receive(obj, authed_as=None, internal=False)[source]
Handles an incoming activity.
If
obj
’s key is unset,obj.as1
’s id field is used. If both are unset, raiseswerkzeug.exceptions.BadRequest
.- Parameters:
obj (models.Object)
authed_as (str) – authenticated actor id who sent this activity
internal (bool) – whether to allow activity ids on internal domains
- Returns:
(response body, HTTP status code) Flask response
- Return type:
- Raises:
werkzeug.HTTPException – if the request is invalid
- classmethod handle_follow(obj)[source]
Handles an incoming follow activity.
Sends an
Accept
back, but doesn’t send theFollow
itself. That happens indeliver()
.- Parameters:
obj (models.Object) – follow activity
- classmethod maybe_accept_follow(follower, followee, follow)[source]
Sends an accept activity for a follow.
…if the follower protocol handles accepts. Otherwise, does nothing.
- Parameters:
follower –
models.User
followee –
models.User
follow –
models.Object
- classmethod bot_follow(user)[source]
Follow a user from a protocol bot user.
…so that the protocol starts sending us their activities, if it needs a follow for that (eg ActivityPub).
- Parameters:
user (User)
- classmethod handle_bare_object(obj)[source]
If obj is a bare object, wraps it in a create or update activity.
Checks if we’ve seen it before.
- Parameters:
obj (models.Object)
- Returns:
obj
if it’s an activity, otherwise a new object- Return type:
- classmethod deliver(obj, from_user)[source]
Delivers an activity to its external recipients.
- Parameters:
obj (models.Object) – activity to deliver
from_user (models.User) – user (actor) this activity is from
- classmethod targets(obj, from_user)[source]
Collects the targets to send a
models.Object
to.Targets are both objects - original posts, events, etc - and actors.
- Parameters:
obj (models.Object)
from_user (User)
- Returns:
maps
models.Target
to original (in response to)models.Object
, if any, otherwise None- Return type:
- classmethod load(id, remote=None, local=True, **kwargs)[source]
Loads and returns an Object from memory cache, datastore, or HTTP fetch.
Sets the
new
andchanged
attributes if we know either one for the loaded object, ie local is True and remote is True or None.Note that
Object._post_put_hook()
updates the cache.- Parameters:
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 (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
fetch()
- Returns:
loaded object, or None if it isn’t fetchable, eg a non-URL string for Web, or
remote
is False and it isn’t in the cache or datastore- Return type:
- Raises:
requests.HTTPError – anything that
fetch()
raises
- protocol.receive_task()[source]
Task handler for a newly received
models.Object
.Calls
Protocol.receive()
with the form parameters.- Parameters:
obj (url-safe google.cloud.ndb.key.Key) –
models.Object
to handleauthed_as (str) – passed to
Protocol.receive()
TODO: migrate incoming webmentions and AP inbox deliveries to this. The difficulty is that parts of
protocol.Protocol.receive()
depend on setup inweb.webmention()
andactivitypub.inbox()
, egmodels.Object
withnew
andchanged
, HTTP request details, etc. See stash for attempt at this forweb.Web
.
- protocol.send_task()[source]
Task handler for sending an activity to a single specific destination.
Calls
Protocol.send()
with the form parameters.- Parameters:
url (str) – destination URL to send to
obj (url-safe google.cloud.ndb.key.Key) –
models.Object
to sendorig_obj (url-safe google.cloud.ndb.key.Key) – optional “original object”
models.Object
that this object refers to, eg replies to or reposts or likesuser (url-safe google.cloud.ndb.key.Key) –
models.User
(actor) this activity is from
redirect
Simple conneg endpoint that serves AS2 or redirects to to the original post.
Only for web.Web
users. Other protocols (including 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:
https://github.com/snarfed/bridgy-fed/issues/16#issuecomment-424799599
https://github.com/tootsuite/mastodon/pull/6219#issuecomment-429142747
The conneg makes these /r/
URLs searchable in Mastodon:
https://github.com/snarfed/bridgy-fed/issues/352
render
web
Webmention protocol with microformats2 in HTML, aka the IndieWeb stack.
- web.is_valid_domain(domain, allow_internal=True)[source]
Returns True if this is a valid domain we can use, False otherwise.
- Parameters:
Valid means TLD is ok, not blacklisted, etc.
- class web.Web(**kwargs)[source]
-
Web user and webmention protocol implementation.
The key name is the domain.
- classmethod get_or_create(id, **kwargs)[source]
Normalize domain, then pass through to
User.get_or_create()
.Normalizing currently consists of lower casing and removing leading and trailing dots.
- profile_id()
Returns this user’s web URL aka web_url, eg
https://foo.com/
.
- username()[source]
Returns the user’s preferred username.
Uses stored representative h-card if available, falls back to id.
- Return type:
- verify()[source]
Fetches site a couple ways to check for redirects and h-card.
- Returns:
user that was verified. May be different than self! eg if self ‘s domain started with www and we switch to the root domain.
- Return type:
- classmethod key_for(id)[source]
Returns the
ndb.Key
for a given id.If id is a domain, uses it as is. If it’s a home page URL or fed.brid.gy or web.brid.gy AP actor URL, extracts the domain and uses that. Otherwise, returns None.
- Parameters:
id (str)
Returns: ndb.Key or None:
- classmethod owns_id(id)[source]
Returns None if id is a domain or http(s) URL, False otherwise.
All web pages are http(s) URLs, but not all http(s) URLs are web pages.
- classmethod owns_handle(handle, allow_internal=False)[source]
Returns whether this protocol owns the handle, or None if it’s unclear.
To be implemented by subclasses.
Handles are string identities that are human-chosen, human-meaningful, and often but not always unique. Compare to IDs, which uniquely identify users, and are intended primarily to be machine readable and usable.
Some protocols’ handles are more or less deterministic based on the id format, eg ActivityPub (technically WebFinger) handles are
@user@instance.com
. Others, like domains, could be owned by eg Web, ActivityPub, AT Protocol, or others.This should be a quick guess without expensive side effects, eg no external HTTP fetches to fetch the id itself or otherwise perform discovery.
- classmethod handle_to_id(handle)[source]
Converts a handle to an id.
To be implemented by subclasses.
May incur network requests, eg DNS queries or HTTP requests.
- classmethod send(obj, url, from_user=None, orig_obj=None, **kwargs)[source]
Sends a webmention to a given target URL.
See
Protocol.send()
for details.Returns False if the target URL doesn’t advertise a webmention endpoint, or if webmention/microformats2 don’t support the activity type. https://fed.brid.gy/docs#error-handling
- classmethod fetch(obj, gateway=False, check_backlink=False, **kwargs)[source]
Fetches a URL over HTTP and extracts its microformats2.
Follows redirects, but doesn’t change the original URL in
obj
’s id!google.cloud.ndb.model.Model
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
Protocol.fetch()
for other background.- Parameters:
gateway (bool) – passed through to
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
- web.webmention_external()[source]
Handles inbound webmention, enqueue task to process.
Use a task queue to deliver to followers because we send to each inbox in serial, which can take a long time with many followers/instances.
webfinger
Handles requests for WebFinger endpoints.
- class webfinger.Webfinger[source]
Bases:
XrdOrJrd
Serves a user’s WebFinger profile.
Supports both JRD and XRD; defaults to JRD. https://tools.ietf.org/html/rfc7033#section-4
- class webfinger.HostMeta[source]
Bases:
XrdOrJrd
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
- DEFAULT_TYPE = 'xrd'
Either
JRD
orwhich
, the type to return by default if the request doesn’t ask for one explicitly with the Accept header.
- webfinger.host_meta_xrds()[source]
Renders and serves the
/.well-known/host-meta.xrds
XRDS-Simple file.
- webfinger.fetch(addr)[source]
Fetches and returns an address’s WebFinger data.
On failure, flashes a message and returns None.
TODO: switch to raising exceptions instead of flashing messages and returning None