Bridgy Fed

Reference documentation.

activitypub

ActivityPub protocol implementation.

class activitypub.ActivityPub(**kwargs)[source]

Bases: User, Protocol

ActivityPub protocol class.

Key id is AP/AS2 actor id URL. (Not fediverse/WebFinger @-@ handle!)

web_url()[source]

Returns this user’s web URL aka web_url, eg https://foo.com/.

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/ .

https://docs.microblog.pub/user_guide.html#activitypub

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 handle_to_id(handle)[source]

Looks in the datastore first, then queries WebFinger.

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 into cc.

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:

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.

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

bool

Raises:
classmethod verify_signature(activity)[source]

Verifies the current request’s HTTP Signature.

Raises werkzeug.exceptions.HTTPError if the signature is missing or invalid, otherwise does nothing and returns the id of the actor whose key signed the request.

Logs details of the result.

Parameters:

activity (dict) – AS2 activity

Returns:

signing AP actor id

Return type:

str

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() or util.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:

requests.Response

activitypub.postprocess_as2(activity, orig_obj=None, wrap=True)[source]

Prepare an AS2 object to be served or sent via ActivityPub.

Parameters:
  • 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 (bool) – whether to wrap id, url, object, actor, and attributedTo

activitypub.postprocess_as2_actor(actor, user)[source]

Prepare an AS2 actor object to be served or sent via ActivityPub.

Modifies actor in place.

Parameters:
Returns:

actor dict

activitypub.actor(handle_or_id)[source]

Serves a user’s AS2 actor from the datastore.

activitypub.inbox(protocol=None, id=None)[source]

Handles ActivityPub inbox delivery.

activitypub.follower_collection(id, collection)[source]

ActivityPub Followers and Following collections.

TODO: unify page generation with outbox()

activitypub.outbox(id)[source]

Serves a user’s AP outbox.

TODO: unify page generation with follower_collection()

atproto

ATProto protocol implementation.

https://atproto.com/

atproto.did_to_handle(did)[source]

Resolves a DID to a handle _if_ we have the DID doc stored locally.

Parameters:

did (str)

Returns:

handle, or None

Return type:

str

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 is bsky.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 send cursor + 1.

class atproto.ATProto(**kwargs)[source]

Bases: User, Protocol

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.

Parameters:

id (str)

Return type:

bool or None

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.

Parameters:
  • handle (str)

  • allow_internal (bool) – whether to return False for internal domains like fed.brid.gy, bsky.brid.gy, etc

Returns:

bool or None

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.

Parameters:

handle (str)

Returns:

corresponding id, or None if the handle can’t be found

Return type:

str

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:

str

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 user alice.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.

classmethod pds_for(obj)[source]

Returns the PDS URL for the given object, or None.

Parameters:

obj (Object)

Return type:

str

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:
  • url (str)

  • allow_internal (bool) – whether to return False for internal domains like fed.brid.gy, bsky.brid.gy, etc

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:

bool

classmethod create_report(input, from_user)[source]

Sends a createReport for a flag activity.

Parameters:
  • input (dict) – createReport input

  • from_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:

bool

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

atproto.poll_posts()[source]

Fetches and enqueueus bridged Bluesky users’ new posts from the AppView.

Uses the getAuthorFeed endpoint, which is intended for clients. 🤷

TODO: unify with poll_notifications

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 in User.public_pem() and User.private_pem().

common.long_to_base64(x)[source]

Converts from long integer to base64 URL safe encoding.

Originally from django_salmon.magicsigs. Used in User.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.

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

Returns:

redirect url

Return type:

str

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, returns https://ap.brid.gy/foo/bar.

Parameters:

proto (subclass of protocol.Protocol)

Returns:

URL

Return type:

str

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.

Parameters:
  • val (str or dict or list)

  • field (str) – optional field name for this value

Returns:

unwrapped url

Return type:

str

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/701

Example: 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 to seq 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 to seq 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 is True, 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:
convert.render_redirect()[source]

Redirect from old /render?id=… endpoint to /convert/…

convert.convert_source_path_redirect(src, dest, _)[source]

Old route that included source protocol in path instead of subdomain.

DEPRECATED! Only kept to support old webmention source URLs.

follow

Remote follow handler.

follow.remote_follow()[source]

Discovers and redirects to a remote follow page for a given user.

class follow.FollowStart(to_path, scopes=None)[source]

Bases: Start

Starts the IndieAuth flow to add a follower to an existing user.

dispatch_request()[source]

The actual view function behavior. Subclasses must override this and return a valid response. Any variables from the URL rule are passed as keyword arguments.

class follow.FollowCallback(to_path, scopes=None)[source]

Bases: Callback

IndieAuth callback to add a follower to an existing user.

finish(auth_entity, state=None)[source]

Called when the OAuth flow is complete. Clients may override.

Parameters:
  • auth_entity (models.BaseAuth) – resulting auth entity, or None if the user declined the site’s OAuth authorization request.

  • state (str) – passed to Start.redirect_url()

Return type:

werkzeug.wrappers.Response

class follow.UnfollowStart(to_path, scopes=None)[source]

Bases: Start

Starts the IndieAuth flow to remove a follower from an existing user.

dispatch_request()[source]

The actual view function behavior. Subclasses must override this and return a valid response. Any variables from the URL rule are passed as keyword arguments.

class follow.UnfollowCallback(to_path, scopes=None)[source]

Bases: Callback

IndieAuth callback to remove a follower.

finish(auth_entity, state=None)[source]

Called when the OAuth flow is complete. Clients may override.

Parameters:
  • auth_entity (models.BaseAuth) – resulting auth entity, or None if the user declined the site’s OAuth authorization request.

  • state (str) – passed to Start.redirect_url()

Return type:

werkzeug.wrappers.Response

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 Objects and Users elsewhere, eg at:// URIs for ATProto records, nevent etc bech32-encoded Nostr ids, ATProto user DIDs, etc.

Used in google.cloud.ndb.model.StructuredPropertys inside Object and 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.

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 the PROTOCOLS 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:

__init__(**kwargs)[source]

Constructor.

Sets obj explicitly because however google.cloud.ndb.model.Model sets it doesn’t work with @property and @obj.setter below.

classmethod new(**kwargs)[source]

Try to prevent instantiation. Use subclasses instead.

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.

Parameters:
  • propagate (bool) – whether to create copies of this user in push-based protocols, eg ATProto and Nostr.

  • allow_opt_out (bool) – whether to allow and create the user if they’re currently opted out

Returns:

existing or new user, or None if the user is opted out

Return type:

User

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 supports to_proto by

default, but the user hasn’t explicitly opted into it.

Parameters:
  • to_proto (Protocol subclass)

  • explicit (bool)

Return type:

bool

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)

handle_as(to_proto)[source]

Returns this user’s handle in a different protocol.

Parameters:

to_proto (str or Protocol)

Returns:

str

id_as(to_proto)[source]

Returns this user’s id in a different protocol.

Parameters:

to_proto (str or Protocol)

Returns:

str

handle_or_id()[source]

Returns handle if we know it, otherwise id.

public_pem()[source]
Return type:

bytes

private_pem()[source]
Return type:

bytes

name()[source]

Returns this user’s human-readable name, eg Ryan Barrett.

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

Parameters:
  • url (str)

  • ignore_www (bool) – if True, ignores www. subdomains

Return type:

bool

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:

str

user_page_path(rest=None)[source]

Returns the user’s Bridgy Fed user page path.

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:

protoProtocol subclass

Return type:

str

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()?

Parameters:
  • handle (bool) – include handle as well as display name

  • maybe_internal_link (bool) – if True, link to Bridgy Fed user page instead of external account

profile_picture()[source]

Returns the user’s profile picture image URL, if available, or None.

count_followers()[source]

Counts this user’s followers and followings.

Returns:

(number of followers, number following)

Return type:

(int, int) tuple

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 this Object is new/changed.

changed is populated by activity_changed().

__init__(*args, **kwargs)[source]
lock = None

Initialized in __init__, synchronizes property access, :meth:`put`s, etc.

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 the new and changed properties.

Parameters:

actor (str) – if a matching Object already exists, its author or actor must contain this actor id. Implements basic authorization for updates and deletes.

Return type:

Object

put(**kwargs)[source]

Stores this object. Uses self.lock.

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

clear()[source]

Clears the Object.our_as1 properties.

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

Returns a pretty HTML link with the actor’s name and picture.

TODO: unify with User.user_link()?

Parameters:
  • image (bool) – whether to include an img tag with the actor’s picture

  • sized (bool) – whether to set an explicit (width=32) size on the profile picture ``img` tag

  • user (User) – current user

Return type:

str

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 is source_protocol, returns this object’s key id.

Parameters:

protoProtocol subclass

Return type:

str

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 and Object.copies, eg ATProto records and Nostr events that we bridged, to the ids of their original objects in their source protocol, eg at://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_ and to users.

If a matching Follower doesn’t exist in the datastore, creates it first.

Parameters:
Return type:

Follower

static fetch_page(collection, user)[source]

Fetches a page of :class:`Follower`s for a given user.

Wraps fetch_page(). Paging uses the before and after query parameters, if available in the request.

Parameters:
  • collection (str) – followers or following

  • user (User)

Returns:

results, annotated with an extra user attribute that holds the follower or following User, and new str query param values for before and after to fetch the previous and next pages, respectively

Return type:

(list of Follower, str, str) tuple

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 returned Object entities for rendering in objects.html.

Parameters:
  • query (ndb.Query)

  • by (ndb.model.Property) – either Object.updated or Object.created

  • user (User) – current user

Returns:

(results, new before query param, new after query param) to fetch the previous and next pages, respectively

Return type:

(list of Object, str, str) tuple

models.fetch_page(query, model_class, by=None)[source]

Fetches a page of results from a datastore query.

Uses the before and after query params (if provided; should be ISO8601 timestamps) and the by 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:
Returns:

(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

Return type:

(list of Object or Follower, str, str) tuple

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`.

Parameters:
  • copy_id (str)

  • keys_only (bool) – passed through to google.cloud.ndb.Query

Return type:

User or Object

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`.

Parameters:
  • copy_ids (sequence of str)

  • keys_only (bool) – passed through to google.cloud.ndb.Query

Returns:

sequence of User and/or Object

pages

UI pages.

pages.load_user(protocol, id)[source]

Loads and returns the current request’s user.

Parameters:
Return type:

models.User

Raises:

werkzeug.exceptions.HTTPException

pages.front_page()[source]

View for the front page.

pages.docs()[source]

View for the docs page.

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, or rss

  • 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:

str or (str, dict) tuple

pages.nodeinfo_jrd()[source]

https://nodeinfo.diaspora.software/protocol.html

pages.nodeinfo()[source]

https://nodeinfo.diaspora.software/schema.html

pages.instance_info()[source]

https://docs.joinmastodon.org/methods/instance/#v1

protocol

Base protocol class and common code.

class protocol.Protocol[source]

Bases: object

Base protocol class. Not to be instantiated; classmethods only.

LABEL

human-readable lower case name

Type:

str

OTHER_LABELS

label aliases

Type:

list of str

ABBREV

lower case abbreviation, used in URL paths

Type:

str

PHRASE

human-readable name or phrase. Used in phrases like Follow this person on {PHRASE}

Type:

str

LOGO_HTML

logo emoji or <img> tag

Type:

str

CONTENT_TYPE

MIME type of this protocol’s native data format, appropriate for the Content-Type HTTP header.

Type:

str

HAS_FOLLOW_ACCEPTS

whether this protocol supports explicit accept/reject activities in response to follows, eg ActivityPub

Type:

bool

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:

bool

REQUIRES_AVATAR

whether accounts on this protocol are required to have a profile picture. If they don’t, their User.status will be blocked.

Type:

bool

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 be blocked.

Type:

bool

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, their User.status will be blocked.

DEFAULT_ENABLED_PROTOCOLS

labels of other protocols that are automatically enabled for this protocol to bridge into

Type:

list of str

__init__()[source]
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:

Protocol

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 of brid.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.

Parameters:

id (str)

Return type:

bool or None

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.

Parameters:
  • handle (str)

  • allow_internal (bool) – whether to return False for internal domains like fed.brid.gy, bsky.brid.gy, etc

Returns:

bool or None

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.

Parameters:

handle (str)

Returns:

corresponding id, or None if the handle can’t be found

Return type:

str

classmethod key_for(id)[source]

Returns the google.cloud.ndb.Key for a given id’s models.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

for_id(remote=True)[source]

Returns the protocol for a given id.

Parameters:
  • id (str)

  • remote (bool) – whether to perform expensive side effects like fetching the id itself over the network, or other discovery.

Returns:

matching protocol, or None if no single known protocol definitively owns this id

Return type:

Protocol subclass

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.

Parameters:

handle (str)

Returns:

matching protocol and optional id (if resolved), or (None, None) if no known protocol owns this handle

Return type:

(Protocol subclass, str) tuple

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() returns https://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:

str

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:

bool

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:

bool

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 and application/activity+json for ActivityPub.

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:

str

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:
  • url (str)

  • allow_internal (bool) – whether to return False for internal domains like fed.brid.gy, bsky.brid.gy, etc

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 is ActivityPub, the ATProto URI at://did:plc:abc/coll/123 will be converted to https://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:

dict

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, raises werkzeug.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:

(str, int) tuple

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 the Follow itself. That happens in deliver().

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

models.Object

classmethod deliver(obj, from_user)[source]

Delivers an activity to its external recipients.

Parameters:
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:
Returns:

maps models.Target to original (in response to) models.Object, if any, otherwise None

Return type:

dict

classmethod load(id, remote=None, local=True, **kwargs)[source]

Loads and returns an Object from memory cache, datastore, or HTTP fetch.

Sets the new and changed 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:

models.Object

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:

TODO: migrate incoming webmentions and AP inbox deliveries to this. The difficulty is that parts of protocol.Protocol.receive() depend on setup in web.webmention() and activitypub.inbox(), eg models.Object with new and changed, HTTP request details, etc. See stash for attempt at this for web.Web.

protocol.send_task()[source]

Task handler for sending an activity to a single specific destination.

Calls Protocol.send() with the form parameters.

Parameters:
  • protocol (str) – Protocol to send to

  • url (str) – destination URL to send to

  • obj (url-safe google.cloud.ndb.key.Key) – models.Object to send

  • orig_obj (url-safe google.cloud.ndb.key.Key) – optional “original object” models.Object that this object refers to, eg replies to or reposts or likes

  • user (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:

The conneg makes these /r/ URLs searchable in Mastodon: https://github.com/snarfed/bridgy-fed/issues/352

redirect.redir(to)[source]

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.

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:
  • domain (str)

  • allow_internal (bool) – whether to return True for internal domains like fed.brid.gy, bsky.brid.gy, etc

Valid means TLD is ok, not blacklisted, etc.

class web.Web(**kwargs)[source]

Bases: User, Protocol

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.

handle_as(to_proto)[source]

Special case ActivityPub to use custom username.

id_as(to_proto)[source]

Special case ActivityPub to use ap_subdomain.

web_url()[source]

Returns this user’s web URL aka web_url, eg https://foo.com/.

profile_id()

Returns this user’s web URL aka web_url, eg https://foo.com/.

is_web_url(url)[source]

Returns True if the given URL is this user’s web URL (homepage).

Parameters:
  • url (str)

  • ignore_www (bool) – if True, ignores www. subdomains

Return type:

bool

user_page_path(rest=None)[source]

Always use domain.

username()[source]

Returns the user’s preferred username.

Uses stored representative h-card if available, falls back to id.

Return type:

str

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:

web.Web

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.

Parameters:
  • handle (str)

  • allow_internal (bool) – whether to return False for internal domains like fed.brid.gy, bsky.brid.gy, etc

Returns:

bool or None

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.

Parameters:

handle (str)

Returns:

corresponding id, or None if the handle can’t be found

Return type:

str

classmethod target_for(obj, shared=False)[source]

Returns obj’s id, as a URL webmention target.

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 load(id, **kwargs)[source]

Wrap Protocol.load() to convert domains to homepage URLs.

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:
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.

web.poll_feed_task()[source]

Fetches a Web site’s feed and delivers new/updated posts.

Params:

domain (str): key id of the Web user

web.webmention_task()[source]

Handles inbound webmention task.

Params:

source (str): URL

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

dispatch_request(*args, **kwargs)[source]

The actual view function behavior. Subclasses must override this and return a valid response. Any variables from the URL rule are passed as keyword arguments.

template_prefix()[source]

Returns template filename, without extension.

template_vars()[source]

Returns a dict with template variables.

URL route variables are passed through as kwargs.

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 or which, the type to return by default if the request doesn’t ask for one explicitly with the Accept header.

template_prefix()[source]

Returns template filename, without extension.

template_vars()[source]

Returns a dict with template variables.

URL route variables are passed through as kwargs.

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

Parameters:

addr (str) – a Webfinger-compatible address, eg @x@y, acct:x@y, or https://x/y

Returns:

fetched WebFinger data, or None on error

Return type:

dict

webfinger.fetch_actor_url(addr)[source]

Fetches and returns a WebFinger address’s ActivityPub actor URL.

On failure, flashes a message and returns None.

Parameters:

addr (str) – a Webfinger-compatible address, eg @x@y, acct:x@y, or https://x/y

Returns:

ActivityPub actor URL, or None on error or not fouund

Return type:

str