Bridgy Fed

Reference documentation.

activitypub

ActivityPub protocol implementation.

class ActivityPub(**kwargs)[source]

Bases: User, Protocol

ActivityPub protocol class.

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

ABBREV = 'ap'
PHRASE = 'the fediverse'
LOGO_HTML = '<img src="/static/fediverse_logo.svg">'
CONTENT_TYPE = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
REQUIRES_AVATAR = True
REQUIRES_NAME = False
DEFAULT_ENABLED_PROTOCOLS = ('web',)
SUPPORTED_AS1_TYPES = ('organization', 'group', 'person', 'application', 'service', 'note', 'article', 'mention', 'comment', 'link', 'post', 'delete', 'undo', 'update', 'rsvp-interested', 'like', 'block', 'stop-following', 'reject', 'flag', 'accept', 'rsvp-no', 'rsvp-maybe', 'share', 'react', 'undo', 'follow', 'rsvp-yes', 'invite', 'audio', 'bookmark', 'image', 'move', 'video')
SUPPORTED_AS2_TYPES = ('Organization', 'Group', 'Person', 'Application', 'Service', 'Note', 'Article', 'Mention', 'Note', 'Link', 'Create', 'Delete', 'Undo', 'Update', None, 'Like', 'Block', None, 'Reject', 'Flag', 'Accept', 'Reject', 'TentativeAccept', 'Announce', None, 'Undo', 'Follow', 'Accept', 'Invite', 'Audio', None, 'Image', 'Move', 'Video')
SUPPORTS_DMS = True
webfinger_addr

Populated by reload_profile().

web_url()[source]

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

reload_profile(**kwargs)[source]

Reloads this user’s AP actor, then resolves their webfinger subject.

  1. load AP actor

  2. fetch Webfinger with preferredUsername

  3. re-fetch Webfinger with subject from first Webfinger

https://www.w3.org/community/reports/socialcg/CG-FINAL-apwf-20240608/#reverse-discovery https://correct.webfinger-canary.fietkau.software/#developers

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.

user_page_path(rest=None, **kwargs)[source]

Always prefer handle, since id is a full URL.

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_id=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 (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 _convert(obj, orig_obj=None, from_user=None)[source]

Convert a models.Object to AS2.

Parameters:
  • obj (Object)

  • orig_obj (dict) – AS2 object, optional. The target of activity’s inReplyTo or Like/Announce/etc object, if any. Passed through to postprocess_as2().

  • from_user (User) – user (actor) this activity/object is from

Returns:

AS2 JSON

Return type:

dict

classmethod migrate_out(user, to_user_id)[source]

Migrates a bridged account out to be a native account.

Parameters:
Raises:

ValueError – eg if ActivityPub doesn’t own to_user_id

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.

https://swicg.github.io/activitypub-http-signature/

Parameters:

activity (dict) – AS2 activity

Returns:

signing AP actor id

Return type:

str

signed_request(fn, url, data=None, headers=None, from_user=None, _redirect_count=None, **kwargs)[source]

Wraps requests.* and adds HTTP Signature.

https://swicg.github.io/activitypub-http-signature/

Parameters:
  • fn (callable) – util.requests_get() or util.requests_post()

  • url (str)

  • data (dict) – optional AS2 object

  • from_user (User) – user to sign request as; optional. If not provided, uses the default user @fed.brid.gy@fed.brid.gy.

  • _redirect_count – internal, used to count redirects followed so far

  • kwargs – passed through to requests

Return type:

Response

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

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

TODO: get rid of orig_obj! https://github.com/snarfed/bridgy-fed/issues/1257

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

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 (User) – current user

Returns:

actor dict

actor(handle_or_id)[source]

Serves a user’s AS2 actor from the datastore.

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

Handles ActivityPub inbox delivery.

follower_collection(id, collection)[source]

ActivityPub Followers and Following collections.

TODO: unify page generation with outbox()

outbox(id)[source]

Serves a user’s AP outbox.

TODO: unify page generation with follower_collection()

atproto

ATProto protocol implementation.

https://atproto.com/

chat_client(*, repo, method, **kwargs)[source]

Returns a new Bluesky chat Client for a given XRPC method.

Parameters:
  • repo (Repo) – ATProto user

  • method (str) – XRPC method NSID, eg chat.bsky.convo.sendMessage

  • kwargs – passed through to the lexrpc.client.Client constructor

Return type:

Client

class DatastoreClient(*args, **kwargs)[source]

Bases: Client

Bluesky client that uses the datastore as well as remote XRPC calls.

Overrides getRecord and resolveHandle. If we have a record or DID document stored locally, uses it as is instead of making a remote XRPC call. Otherwise, passes through to the server.

Right now, requires that the server address is the same as $APPVIEW_HOST, because getRecord passes through to ATProto.load and then to ATProto.fetch, which uses the appview global.

did_to_handle(did, remote=None)[source]

Resolves a DID to a handle.

Parameters:
  • did (str)

  • remote (bool) – whether to fetch the object over the network. See Protocol.load()

Returns:

handle, or None

Return type:

str

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

cursor
created
updated
class 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

ABBREV = 'bsky'
PHRASE = 'Bluesky'
LOGO_HTML = '<img src="/oauth_dropins_static/bluesky.svg">'
PDS_URL = 'https://atproto.brid.gy'

Note that PDS hostname is atproto.brid.gy here, not bsky.brid.gy. Bluesky team currently has our hostname as atproto.brid.gy in their federation test. also note that PDS URL shouldn’t include trailing slash. https://atproto.com/specs/did#did-documents

CONTENT_TYPE = 'application/json'
HAS_COPIES = True
REQUIRES_AVATAR = True
REQUIRES_NAME = False
DEFAULT_ENABLED_PROTOCOLS = ('web',)
SUPPORTED_AS1_TYPES = frozenset({'application', 'article', 'block', 'comment', 'delete', 'flag', 'follow', 'group', 'like', 'link', 'mention', 'note', 'organization', 'person', 'post', 'service', 'share', 'stop-following', 'undo', 'update'})
SUPPORTED_RECORD_TYPES = frozenset({'app.bsky.actor.profile', 'app.bsky.feed.like', 'app.bsky.feed.post', 'app.bsky.feed.repost', 'app.bsky.graph.block', 'app.bsky.graph.follow'})
SUPPORTS_DMS = True
reload_profile(**kwargs)[source]

Reloads this user’s DID doc along with their profile object.

classmethod bridged_web_url_for(user, fallback=False)[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.

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

classmethod create_for(user)[source]

Creates an ATProto repo and profile for a non-ATProto user.

If the repo already exists, reactivates it by emitting an #account event with active: True.

Parameters:

user (User)

Raises:

ValueError – if the user’s handle is invalid, eg begins or ends with an underscore or dash

classmethod set_dns(handle, did)[source]

Create _atproto DNS record for handle resolution.

https://atproto.com/specs/handle#handle-resolution

If the DNS record already exists, or if we’re not in prod, does nothing. If the DNS record exists with a different DID, deletes it and recreates it with this DID.

Parameters:

handle (str) – Bluesky handle, eg snarfed.org.web.brid.gy

classmethod remove_dns(handle)[source]

Removes an _atproto DNS record.

https://atproto.com/specs/handle#handle-resolution

Parameters:

handle (str) – Bluesky handle, eg snarfed.org.web.brid.gy

classmethod send(obj, url, from_user=None, orig_obj_id=None)[source]

Creates a record if we own its repo.

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.

Exceptions: * flag``s are translated to ``createReport to the mod service * DMs are translated to sendMessage to the chat service

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 (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 _convert(obj, fetch_blobs=False, from_user=None)[source]

Converts a models.Object to app.bsky.* lexicon JSON.

Parameters:
  • obj (Object)

  • fetch_blobs (bool) – whether to fetch images and other blobs, store them in arroba.datastore_storage.AtpRemoteBlobs if they don’t already exist, and fill them into the returned object.

  • from_user (User) – user (actor) this activity/object is from

Returns:

JSON object

Return type:

dict

classmethod migrate_in(user, from_user_id, plc_code, pds_client)[source]

Migrates an ATProto account on another PDS in to be a bridged account.

https://atproto.com/guides/account-migration

Before calling this, the repo must have already been imported with com.atproto.repo.importRepo!

Parameters:
  • user (User) – native user on another protocol to attach the newly imported bridged account to

  • from_user_id (str) – DID of the account to be migrated in

  • plc_code (str) – a PLC operation confirmation code from the account’s old PDS, from com.atproto.identity.requestPlcOperationSignature

  • pds_client (lexrpc.Client) – authenticated client for the account’s old PDS

Raises:

ValueError – if from_user_id is not an ATProto DID, or user is an ATProto, or user is already bridged to Bluesky, or the repo hasn’t been imported yet

Adds “bridged from … by Bridgy Fed” text to obj.our_as1.

Overrides the default protocol.Protocol.add_source_links() implementation to use plain text URLs because app.bsky.actor.profile has no descriptionFacets for the description field.

TODO: much of this duplicates protocol.Protocol.add_source_links(). Refactor somehow.

Parameters:
  • obj (Object)

  • from_user (User) – user (actor) this activity/object is from

create_report(*, input, from_user)[source]

Sends a createReport for a flag activity.

Parameters:
  • input (dict) – createReport input

  • from_user (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

send_chat(*, msg, from_repo, to_did)[source]

Sends a chat message to this user.

Parameters:
  • msg (dict) – chat.bsky.convo.defs#messageInput

  • from_repo (Repo)

  • to_did (str)

Returns:

True if the message was sent successfully, False otherwise, eg

if the recipient has disabled chat

Return type:

bool

poll_chat_task()[source]

Polls for incoming chat messages for our protocol bot users.

Params:

proto: protocol label, eg activitypub

atproto_did()[source]

Programmatic handle resolution for bridged users.

https://github.com/snarfed/bridgy-fed/issues/1537 https://atproto.com/specs/handle#handle-resolution

Query params:

protocol (str) id (str): native user id or handle

atproto_firehose

ATProto firehose client. Enqueues receive tasks for events for bridged users.

https://atproto.com/specs/event-stream https://atproto.com/specs/sync#firehose

class Op(action, repo, path, seq, record, time)

Bases: tuple

subscriber()[source]

Wrapper around _subscribe() that catches exceptions and reconnects.

subscribe()[source]

Subscribes to the relay’s firehose.

Relay hostname comes from the BGS_HOST environment variable.

Parameters:

reconnect (bool) – whether to always reconnect after we get disconnected

handler()[source]

Wrapper around handle() that catches exceptions and restarts.

common

Misc common utilities.

protocol_user_copy_ids()[source]

Returns all copy ids for protocol bot users.

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

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

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

  • text (str)

  • user (User) – current user

  • kwargs – passed through to oauth_dropins.webutil.util.pretty_link()

content_type(resp)[source]

Returns a requests.Response’s Content-Type, without charset suffix.

redirect_wrap(url, domain=None)[source]

Returns a URL on our domain that redirects to this URL.

…to satisfy Mastodon’s non-standard domain matching requirement. :(

Parameters:
  • url (str)

  • domain (str) – optional Bridgy Fed domain to use. Must be in DOMAINS

Returns:

redirect url

Return type:

str

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

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

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:

Response or (str, int)

report_error(msg, *, exception=False, **kwargs)[source]

Reports an error to StackDriver Error Reporting.

https://cloud.google.com/python/docs/reference/clouderrorreporting/latest/google.cloud.error_reporting.client.Client

If DEBUG and exception are True, re-raises the exception instead.

Duplicated in bridgy.util.

cache_policy(key)[source]

In memory ndb cache.

https://github.com/snarfed/bridgy-fed/issues/1149#issuecomment-2261383697

Only cache kinds in memory that are immutable or largely harmless when changed.

Keep an eye on this in case we start seeing problems due to this ndb bug where unstored in-memory modifications get returned by later gets: https://github.com/googleapis/python-ndb/issues/888

Parameters:

key (google.cloud.datastore.key.Key or Key) – see https://github.com/googleapis/python-ndb/issues/987

Returns:

whether to cache this object

Return type:

bool

global_cache_timeout_policy(key)[source]

Cache everything for 2h.

Parameters:

key (google.cloud.datastore.key.Key or Key) – see https://github.com/googleapis/python-ndb/issues/987

Returns:

cache expiration for this object, in seconds

Return type:

int

as2_request_type()[source]

If this request has conneg (ie the Accept header) for AS2, returns its type.

Specifically, returns either application/ld+json; profile="https://www.w3.org/ns/activitystreams" or application/activity+json.

If the current request’s conneg isn’t asking for AS2, returns None.

https://www.w3.org/TR/activitypub/#retrieving-objects https://snarfed.org/2023-03-24_49619-2

log_request()[source]

Logs GET query params and POST form.

Limits each value to 1000 chars.

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(to, _, from_=None)[source]

Converts data from one protocol to another and serves it.

Fetches the source data if it’s not already stored.

Note that we don’t do conneg or otherwise care about the Accept header here, we always serve the “to” protocol’s format.

Parameters:
check_bridged_to(obj, to_proto)[source]

If object or its owner isn’t bridged to to_proto, raises werkzeug.exceptions.HTTPException.

Parameters:
convert_source_path_redirect(from_, to, _)[source]

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

DEPRECATED! Only kept to support old webmention source URLs.

dms

Protocol-independent code for sending and receiving DMs aka chat messages.

command(names, arg=False, user_bridged=None, handle_bridged=None)[source]

Function decorator. Defines and registers a DM command.

Parameters:
  • names (sequence of str) – the command strings that trigger this command, or None if this command has no command string

  • arg – whether this command takes an argument. False for no, True for yes, anything, 'handle' for yes, a handle in the bot account’s protocol for a user that must not already be bridged.

  • user_bridged (bool) – whether the user sending the DM should be bridged. True for yes, False for no, ``None` for either.

  • handle_bridged (bool) – whether the handle arg should be bridged. True for yes, False for no, ``None` for either.

The decorated function should have the signature:

(from_user, to_proto, arg=None, to_user=None) => (str, None)

If it returns a string, that text is sent to the user as a reply to their DM.

Args for the decorated function:

from_user (models.User): the user who sent the DM to_proto (protocol.Protocol): the protocol bot account they sent it to arg (str or None): the argument to the command, if any to_user (models.User or None): the user for the argument, if it’s a handle

The decorated function returns:

str: text to reply to the user in a DM, if any

maybe_send(*, from_proto, to_user, text, type=None, in_reply_to=None)[source]

Sends a DM.

Creates a task to send the DM asynchronously.

If type is provided, and we’ve already sent this user a DM of this type from this protocol, does nothing.

Parameters:
  • from_proto (Protocol)

  • to_user (User)

  • text (str) – message content. May be HTML.

  • type (str) – optional, one of DM.TYPES

  • in_reply_to (str) – optional, id of a DM to reply to

receive(*, from_user, obj)[source]

Handles a DM that a user sent to one of our protocol bot users.

Parameters:
Returns:

(response body, HTTP status code) Flask response

Return type:

(str, int) tuple

load_user(proto, handle)[source]
Parameters:
Returns:

models.User or None

follow

Remote follow handler.

remote_follow()[source]

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

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

Bases: Start

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

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

Bases: Callback

IndieAuth callback to add a follower to an existing user.

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

Bases: Start

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

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

Bases: Callback

IndieAuth callback to remove a follower.

ids

Translates user ids, handles, and object ids between protocols.

https://fed.brid.gy/docs#translate

validate(id, from_, to)[source]

Validates args.

Asserts that all args are non-None. If from_ or to are instances, returns their classes.

web_ap_base_domain(user_domain)[source]

Returns the full Bridgy Fed domain to use for a given Web user.

Specifically, returns http://localhost/` if we're running locally, ``https://[ap_subdomain].brid.gy/ for the Web entity for this domain if it exists, otherwise https://web.brid.gy/.

Parameters:

user_domain (str)

Return type:

str

translate_user_id(*, id, from_, to)[source]

Translate a user id from one protocol to another.

TODO: unify with translate_object_id().

Parameters:
Returns:

the corresponding id in to

Return type:

str

normalize_user_id(*, id, proto)[source]

Normalizes a user id to its canonical representation in a given protocol.

Examples:

  • Web: * user.com => user.com * www.user.com => user.com * https://user.com/ => user.com

  • ATProto: * did:plc:123 => did:plc:123 * https://bsky.app/profile/did:plc:123 => did:plc:123

Note that profile_id() is a narrower inverse of this; it converts user ids to profile ids.

Parameters:
Returns:

the normalized user id

Return type:

str

profile_id(*, id, proto)[source]

Returns the profile object id for a given user id.

Examples:

Note that normalize_user_id() does the inverse of this, ie converts profile ids to user ids.

Parameters:
Returns:

the profile id

Return type:

str

translate_handle(*, handle, from_, to, enhanced)[source]

Translates a user handle from one protocol to another.

Parameters:
  • handle (str)

  • from (Protocol)

  • to (Protocol)

  • enhanced (bool) – whether to convert to an “enhanced” handle based on the user’s domain

TODO: drop enhanced arg, always use if available?

Returns:

the corresponding handle in to

Return type:

str

Raises:

ValueError – if the user’s handle is invalid, eg begins or ends with an underscore or dash

translate_object_id(*, id, from_, to)[source]

Translates a user handle from one protocol to another.

TODO: unify with translate_user_id().

Parameters:
Returns:

the corresponding id in to

Return type:

str

memcache

Utilities for caching data in memcache.

key(key)[source]

Preprocesses a memcache key. Right now just truncates it to 250 chars.

https://pymemcache.readthedocs.io/en/latest/apidoc/pymemcache.client.base.html https://github.com/memcached/memcached/wiki/Commands#standard-protocol

TODO: truncate to 250 UTF-8 chars, to handle Unicode chars in URLs. Related: pymemcache Client’s allow_unicode_keys constructor kwarg.

Parameters:

key (str)

Return type:

bytes

memoize(expire=None, key=None, write=True, version=2)[source]

Memoize function decorator that stores the cached value in memcache.

Parameters:
  • expire (timedelta) – optional, expiration

  • key (callable) – function that takes the function’s (*args, **kwargs) and returns the cache key to use. If it returns None, memcache won’t be used.

  • write (bool or callable) – whether to write to memcache. If this is a callable, it will be called with the function’s (*args, **kwargs) and should return True or False.

  • version (int) – overrides our default version number in the memcache key. Bumping this version can have the same effect as clearing the cache for just the affected function.

models

Datastore model classes.

class 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

uri
protocol
class DM(**kwargs)[source]

Bases: Model

protocol.Protocol + type pairs for identifying sent DMs.

Used in User.sent_dms.

https://googleapis.dev/python/python-ndb/latest/model.html#google.cloud.ndb.model.StructuredProperty

type

Known values (keep in sync with USER_STATUS_DESCRIPTIONS!): * no-feed-or-webmention * opt-out * owns-webfinger * private * replied_to_bridged_user * request_bridging * requires-avatar * requires-name * requires-old-account * unsupported-handle-ap * welcome

protocol
class ProtocolUserMeta(name, bases, class_dict)[source]

Bases: MetaModel

User metaclass. Registers all subclasses in the PROTOCOLS global.

reset_protocol_properties()[source]

Recreates various protocol properties to include choices from PROTOCOLS.

class User(**kwargs)[source]

Bases: StringIdModel

Abstract base class for a Bridgy Fed user.

Stores some protocols’ keypairs. Currently:

obj_key
mod
use_instead
copies

Proxy copies of this user elsewhere, eg DIDs for ATProto records, bech32 npub Nostr ids, etc. Similar to rel-me links in microformats2, alsoKnownAs in DID docs (and now AS2), etc.

public_exponent

Part of this user’s bridged ActivityPub actor’s private key.

private_exponent

Part of this user’s bridged ActivityPub actor’s private key.

manual_opt_out

Set to True for users who asked to be opted out.

enabled_protocols

Protocols that this user has explicitly opted into.

Protocols that don’t require explicit opt in are omitted here. choices is populated in reset_protocol_properties().

sent_dms

DMs that we’ve attempted to send to this user.

created
updated
classmethod new(**kwargs)[source]

Try to prevent instantiation. Use subclasses instead.

add(prop, val)[source]

Adds a value to a multiply-valued property. Uses self.lock.

Parameters:
  • prop (str)

  • val

classmethod get_by_id(id, allow_opt_out=False, **kwargs)[source]

Override to follow use_instead property and status.

Returns None if the user is opted out.

classmethod get_or_create(id, propagate=False, allow_opt_out=False, reload=False, **kwargs)[source]

Loads and returns a User. Creates it if necessary.

Not transactional because transactions don’t read or write memcache. :/ Fortunately we don’t really depend on atomicity for much, last writer wins is usually fine.

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

  • reload (bool) – whether to reload profile always, vs only if necessary

  • kwargs – passed through to cls constructor

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.

delete(proto=None)[source]

Deletes a user’s bridged actors in all protocols or a specific one.

Parameters:

proto (Protocol) – optional

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

Also sends a welcome DM to the user (via a send task) if their protocol supports DMs.

Parameters:

to_proto (protocol.Protocol subclass)

disable_protocol(to_proto)[source]

Removes to_proto` from :attr:`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

id_uri()[source]

Returns the user id as a URI.

Sometimes this is the user id itself, eg ActivityPub actor ids. Sometimes it’s a bit different, eg at://did:plc:… for ATProto user, https://site.com for Web users.

Returns:

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 or None

reload_profile(**kwargs)[source]

Reloads this user’s identity and profile from their native protocol.

Populates the reloaded profile Object in self.obj.

Parameters:

kwargs – passed through to Protocol.load()

user_page_path(rest=None, prefer_id=False)[source]

Returns the user’s Bridgy Fed user page path.

Parameters:
  • rest (str) – additional path and/or query to add to the end

  • prefer_id (bool) – whether to prefer to use the account’s id in the path instead of handle. Defaults to False.

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 HTML link to the user’s profile.

Can optionally include display name, handle, profile picture, and/or link to a different protocol that they’ve enabled.

TODO: unify with Object.actor_link()?

Parameters:
  • name (bool) – include display name

  • handle (bool) – include handle

  • pictures (bool) – include profile picture and protocol logo

  • proto (Protocol) – link to this protocol instead of the user’s native protocol

  • proto_fallback (bool) – if True, and proto is provided and has no no canonical profile URL for bridged users, uses the user’s profile URL in their native protocol

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

Bases: StringIdModel

An activity or other object, eg actor.

Key name is the id. We synthesize ids if necessary.

users

User(s) who created or otherwise own this object.

notify

User who should see this in their user page, eg in reply to, reaction to, share of, etc.

feed

User who should see this in their feeds, eg followers of its creator

source_protocol

The protocol this object originally came from.

choices is populated in reset_protocol_properties(), after all User subclasses are created, so that PROTOCOLS is fully populated.

TODO: nail down whether this is ABBREV` or LABEL

as2

ActivityStreams 2, for ActivityPub

bsky

AT Protocol lexicon, for Bluesky

mf2

HTML microformats2 item, (ie _not_ top level parse object with items field

our_as1

ActivityStreams 1, for activities that we generate or modify ourselves

raw

Other standalone data format, eg DID document

deleted
copies

// URIs for ATProto records and nevent etc bech32-encoded Nostr ids, where this object is the original. Similar to u-syndication links in microformats2 and upstream/downstreamDuplicates in AS1.

Type:

Copies of this object elsewhere, eg at

created
updated
new = None

True if this object is new, ie this is the first time we’ve seen it, False otherwise, None if we don’t know.

changed = None

True if this object’s contents have changed from our existing copy in the datastore, False otherwise, None if we don’t know. Object is new/changed. See activity_changed() for more details.

lock = None

Synchronizes add() and remove().

classmethod get_by_id(id, authed_as=None, **kwargs)[source]

Fetches the Object with the given id, if it exists.

Parameters:
  • id (str)

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

Return type:

Object

Raises:

Forbidden – the existing object

classmethod get_or_create(id, authed_as=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.

Not transactional because transactions don’t read or write memcache. :/ Fortunately we don’t really depend on atomicity for much, last writer wins is usually fine.

Parameters:

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

Return type:

Object

Raises:

Forbidden – the existing object

add(prop, val)[source]

Adds a value to a multiply-valued property. Uses self.lock.

Parameters:
  • prop (str)

  • val

Returns:

True if val was added, ie it wasn’t already in prop, False otherwise

remove(prop, val)[source]

Removes a value from a multiply-valued property. Uses self.lock.

Parameters:
  • prop (str)

  • val

static from_request()[source]

Creates and returns an Object from form-encoded JSON parameters.

Parameters:
to_request()[source]

Returns a query parameter dict representing this Object.

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 Follower(**kwargs)[source]

Bases: Model

A follower of a Bridgy Fed user.

from_

The follower.

to

The followee, ie the user being followed.

follow

The last follow activity.

status

Whether this follow is active or note.

classmethod get_or_create(*, from_, to, **kwargs)[source]

Returns a Follower with the given from_ and to users.

Not transactional because transactions don’t read or write memcache. :/ Fortunately we don’t really depend on atomicity for much, last writer wins is usually fine.

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

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

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

get_original_object_key(copy_id)[source]

Finds the Object with a given copy id, if any.

Note that Object.add() also updates this function’s memcache.memoize() cache.

Parameters:

copy_id (str)

Returns:

google.cloud.ndb.Key or None

get_original_user_key(copy_id)[source]

Finds the user with a given copy id, if any.

Note that User.add() also updates this function’s memcache.memoize() cache.

Parameters:
  • copy_id (str)

  • not_proto (Protocol) – optional, don’t query this protocol

Returns:

google.cloud.ndb.Key or None

pages

UI pages.

load_user(protocol, id)[source]

Loads and returns the current request’s user.

Parameters:
Return type:

User

Raises:

HTTPException

front_page()[source]

View for the front page.

docs()[source]

View for the docs page.

serve_feed(*, objects, format, user, title, as_snippets=False, quiet=False)[source]

Generates a feed based on Object s.

Parameters:
  • objects (sequence of Object)

  • format (str) – html, atom, or rss

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

nodeinfo_jrd()[source]

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

nodeinfo()[source]

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

instance_info()[source]

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

protocol

Base protocol class and common code.

error(*args, status=299, **kwargs)[source]

Default HTTP status code to 299 to prevent retrying task.

class Protocol[source]

Bases: object

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

ABBREV = None

lower case abbreviation, used in URL paths

Type:

str

PHRASE = None

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

Type:

str

OTHER_LABELS = ()

label aliases

Type:

sequence of str

LOGO_HTML = ''

logo emoji or <img> tag

Type:

str

CONTENT_TYPE = None

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

Type:

str

HAS_COPIES = False

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 = False

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 = False

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 = False

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.

Type:

bool

DEFAULT_ENABLED_PROTOCOLS = ()

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

Type:

sequence of str

DEFAULT_SERVE_USER_PAGES = False

whether to serve user pages for all of this protocol’s users on the fed.brid.gy. If False, user pages will only be served for users who have explictly opted in.

Type:

bool

SUPPORTED_AS1_TYPES = ()

AS1 objectTypes and verbs that this protocol supports receiving and sending

Type:

sequence of str

SUPPORTS_DMS = False

whether this protocol can receive DMs (chat messages)

Type:

bool

class property LABEL

human-readable lower case name of this protocol, eg 'activitypub

Type:

str

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 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 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. Avoids blocked or opted out users.

Parameters:

handle (str)

Returns:

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

Return type:

str

classmethod key_for(id, allow_opt_out=False)[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.

Parameters:
  • id (str)

  • allow_opt_out (bool) – whether to allow users who are currently opted out

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

for_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, fallback=False)[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 (User)

  • fallback (bool) – if True, and bridged users have no canonical user profile URL in this protocol, return the native protocol’s profile URL

Returns:

str, or None if there isn’t a canonical URL

classmethod actor_key(obj, allow_opt_out=False)[source]

Returns the User: key for a given object’s author or actor.

Parameters:
  • obj (Object)

  • allow_opt_out (bool) – whether to return a user key if they’re opted out

Return type:

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 or re-activate a copy user in this protocol.

Should add the copy user to copies.

If the copy user already exists and active, should do nothing.

Parameters:

user (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_id=None)[source]

Sends an outgoing activity.

To be implemented by subclasses.

NOTE: if this protocol’s HAS_COPIES is True, and this method creates a copy and sends it, it must add that copy to the object’s (not activity’s) copies!

Parameters:
  • obj (Object) – with activity to send

  • url (str) – destination URL to send to

  • from_user (User) – user (actor) this activity is from

  • orig_obj_id (str) – models.Object key id of 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 (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:

RequestException or 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 (Object)

  • from_user (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 _convert(obj, from_user=None, **kwargs)[source]

Converts an Object to this protocol’s data format.

To be implemented by subclasses. Implementations should generally call Protocol.translate_ids() (as their own class) before converting to their format.

Parameters:
  • obj (Object)

  • from_user (User) – user (actor) this activity/object is from

  • kwargs – protocol-specific

Returns:

converted object in the protocol’s native format, often a dict. May

return the {} empty dict if the object can’t be converted.

Adds “bridged from … by Bridgy Fed” HTML to actor['summary'].

Default implementation; subclasses may override.

Parameters:
  • actor (dict) – AS1 actor

  • obj (Object)

  • from_user (User) – user (actor) this activity/object is from

classmethod set_username(user, username)[source]

Sets a custom username for a user’s bridged account in this protocol.

Parameters:
Raises:
classmethod migrate_out(user, to_user_id)[source]

Migrates a bridged account out to be a native account.

Parameters:
Raises:

ValueError – eg if this protocol doesn’t own to_user_id, or if user is on this protocol or not bridged to this protocol

classmethod migrate_in(user, from_user_id, **kwargs)[source]

Migrates a native account in to be a bridged account.

Parameters:
  • user (User) – native user on another protocol to attach the newly imported bridged account to

  • from_user_id (str)

  • kwargs – additional protocol-specific parameters

Raises:

ValueError – eg if this protocol doesn’t own from_user_id, or if user is on this protocol or already bridged to this protocol

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

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

  • bcc

  • bto

  • cc

  • object

  • object.actor

  • object.author

  • object.id

  • object.inReplyTo

  • object.object

  • attachments[].id

  • tags[objectType=mention].url

  • to

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 AS1 version of obj

Return type:

dict

classmethod receive(obj, authed_as=None, internal=False, received_at=None)[source]

Handles an incoming activity.

If obj’s key is unset, obj.as1’s id field is used. If both are unset, returns HTTP 299.

Parameters:
  • obj (Object)

  • authed_as (str) – authenticated actor id who sent this activity

  • internal (bool) – whether to allow activity ids on internal domains, from opted out/blocked users, etc.

  • received_at (datetime) – when we first saw (received) this activity. Right now only used for monitoring.

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 (Object) – follow activity

classmethod respond_to_follow(verb, follower, followee, follow)[source]

Sends an accept or reject activity for a follow.

…if the follower’s protocol supports accepts/rejects. 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, authed_as=None)[source]

If obj is a bare object, wraps it in a create or update activity.

Checks if we’ve seen it before.

Parameters:
  • obj (Object)

  • authed_as (str) – authenticated actor id who sent this activity

Returns:

obj if it’s an activity, otherwise a new object

Return type:

Object

classmethod deliver(obj, from_user, crud_obj=None, to_proto=None)[source]

Delivers an activity to its external recipients.

Parameters:
  • obj (Object) – activity to deliver

  • from_user (User) – user (actor) this activity is from

  • crud_obj (Object) – if this is a create, update, or delete/undo activity, the inner object that’s being written, otherwise None. (This object’s notify and feed properties may be updated.)

  • to_proto (Protocol) – optional; if provided, only deliver to targets on this protocol

Returns:

Flask response

Return type:

(str, int) tuple

classmethod targets(obj, from_user, crud_obj=None, internal=False)[source]

Collects the targets to send a models.Object to.

Targets are both objects - original posts, events, etc - and actors.

Parameters:
  • obj (Object)

  • from_user (User)

  • crud_obj (Object) – if this is a create, update, or delete/undo activity, the inner object that’s being written, otherwise None. (This object’s notify and feed properties may be updated.)

  • internal (bool) – whether this is a recursive internal call

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, raise_=True, **kwargs)[source]

Loads and returns an Object from 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.

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.

  • raise (bool) – if False, catches any request.RequestException or HTTPException raised by fetch() and returns None instead

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

Return type:

Object

Raises:

HTTPError – anything that fetch() raises, if raise_ is True

classmethod check_supported(obj)[source]

If this protocol doesn’t support this object, raises HTTP 204.

Also reports an error.

(This logic is duplicated in some protocols, eg ActivityPub, so that they can short circuit out early. It generally uses their native formats instead of AS1, before an models.Object is created.)

Parameters:

obj (Object)

Raises:

werkzeug.HTTPException – if this protocol doesn’t support this object

receive_task()[source]

Task handler for a newly received models.Object.

Calls Protocol.receive() with the form parameters.

Parameters:
  • authed_as (str) – passed to Protocol.receive()

  • obj_id (str) – key id of models.Object to handle

  • received_at (str, ISO 8601 timestamp) – when we first saw (received) this activity

  • * – If obj_id is unset, all other parameters are properties for a new models.Object to handle

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

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_id (str) – key id of models.Object to send

  • orig_obj_id (str) – optional, models.Object key id of the “original 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

  • * – If obj_id is unset, all other parameters are properties for a new models.Object to handle

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

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.

web

Webmention protocol with microformats2 in HTML, aka the IndieWeb stack.

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

Bases: User, Protocol

Web user and webmention protocol implementation.

The key name is the domain.

ABBREV = 'web'
PHRASE = 'the web'
OTHER_LABELS = ('webmention',)
LOGO_HTML = '🌐'
CONTENT_TYPE = 'text/html; charset=utf-8'
DEFAULT_ENABLED_PROTOCOLS = ('activitypub',)
DEFAULT_SERVE_USER_PAGES = True
SUPPORTED_AS1_TYPES = ('organization', 'group', 'person', 'application', 'service', 'note', 'article', 'mention', 'comment', 'link', 'post', 'delete', 'undo', 'update', 'audio', 'bookmark', 'event', 'image', 'video', 'follow', 'like', 'share', 'stop-following')
has_redirects
redirects_error
has_hcard

remove?

Type:

Currently unused, and I think now always ends up as True. TODO

last_webmention_in
last_polled_feed
feed_last_item

feed item id (URL)

Type:

str

feed_etag
feed_last_modified
atproto_last_chat_log_cursor

Only used by protocol bot users in Bluesky, for polling their chat messages with chat.bsky.convo.getLog.

ap_subdomain

Originally, BF served Web users’ AP actor ids on fed.brid.gy, eg https://fed.brid.gy/snarfed.org . When we started adding new protocols, we switched to per-protocol subdomains, eg https://web.brid.gy/snarfed.org . However, we need to preserve the old users’ actor ids as is.

Also, our per-protocol bot accounts in ActivityPub are on their own subdomains, eg @bsky.brid.gy@bsky.brid.gy.

So, this property tracks which subdomain a given Web user’s AP actor uses.

classmethod get_or_create(id, allow_opt_out=False, verify=None, **kwargs)[source]

Normalize domain, then pass through to User.get_or_create().

Normalizing currently consists of lower casing and removing leading and trailing dots.

Parameters:

verify (bool) – whether to call verify() to load h-card, check redirects, etc. Defaults to calling it only if the user is new.

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

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 or None

user_page_path(rest=None, **kwargs)[source]

Always prefer domain (id).

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

classmethod key_for(id, allow_opt_out=False)[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)

  • allow_opt_out (bool) – whether to allow users who are currently opted out

Returns: ndb.Key or None:

classmethod owns_id(id)[source]

Returns True on domains and internal URLs, None on other URLs.

All web pages are http(s) URLs, but not all http(s) URLs are web pages.

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

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

feed_url()[source]

Returns this web site’s RSS or Atom feed URL and type, if any.

Return type:

(str, type) or (None, None)

webmention_endpoint()[source]

Returns this web site’s webmention endpoint, if any.

Returns:

webmention endpoint URL

Return type:

str

classmethod send(obj, url, from_user=None, orig_obj_id=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, authorship_fetch_mf2=True, metaformats=None, **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.

  • authorship_fetch_mf2 (bool) – optional, when running the authorship algorithm, fetch author URL if necessary

  • kwargs – ignored

classmethod _convert(obj, from_user=None)[source]

Converts a Object to HTML.

Parameters:
  • obj (Object)

  • from_user (User) – user (actor) this activity/object is from

Return type:

str

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.

poll_feed(user, feed_url, rel_type)[source]

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

Parameters:
  • user (Web)

  • feed_url (str)

  • rel_type (str) – feed link’s top-level rel type in home page HTML, usually either atom or rss

Return type:

list of dict AS1 activities

poll_feed_task()[source]

Task handler for polling a Web user’s feed.

Params:

domain (str): key id of the Web user last_polled (str): should match the user’s last_polled_feed. Used to detect duplicate poll tasks for the same user.

webmention_task()[source]

Handles inbound webmention task.

Params:

source (str): URL

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.

webmention_discover(url, **kwargs)[source]

Thin caching wrapper around oauth_dropins.webutil.webmention.discover().

webfinger

Handles requests for WebFinger endpoints.

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

dispatch_request(**kwargs)[source]

Add the Cache-Control header.

host_meta_xrds()[source]

Renders and serves the /.well-known/host-meta.xrds XRDS-Simple file.

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

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