Bridgy Fed

Reference documentation.

activitypub

ActivityPub protocol implementation.

exception NeedsAlias[source]

Bases: ValueError

Raised when a user can’t migrate out because the destination account is missing an alsoKnownAs alias back to their bridged actor.

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

//github.com/snarfed/bridgy-fed/issues/1608 , https://github.com/snarfed/bridgy-fed/issues/1218

Type:

https

HTML_PROFILES = True
BOTS_FOLLOW_BACK = 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

  4. if a profile URL is a top-level URL (ie home page), check that it serves this AP user, and if so, set verified_domain

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

Returns the user’s bridged AP id.

There’s no single canonical web URL for a user bridged into ActivityPub. So, we want some URL that’s reasonable, and when it’s used in a link in a fediverse post, eg in an @-mention, we ideally want it to open that local instance’s view of the remote bridged user. In general, that means it needs to serve the AP actor when requested with AP conneg.

Our translated AP actor ids, served by /actor below, satisfy this. They serve the actor via conneg, and otherwise redirect to the user’s profile in their native protocol.

classmethod send(obj, inbox_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, use_fetched_id=True, **_)[source]

Tries to fetch an AS2 object.

Assumes obj.key.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.

If the fetched AS2 object’s id is different from obj.key.id, this method defaults to overwriting obj.key.id with the fetched id! You can disable that with use_fetched_id=False.

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.

  • use_fetched_id (bool) – whether to override obj’s key id with the returned object’s id if it differs. Defaults to False.

  • 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, **kwargs)[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

  • kwargs – unused

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 check_can_migrate_out(user, to_user_id)[source]

Raises an exception if a user can’t yet migrate to a native AP account.

For example, if to_user_id isn’t an ActivityPub actor id, or if it doesn’t have user’s bridged AP id in its alsoKnownAs.

Parameters:
Raises:

ValueError – if user can’t migrate to ActivityPub or to_user_id yet

classmethod authed_user_for_request(log_level=10)[source]

Returns the AP actor id of the user who signed the current request.

Verifies the current request’s HTTP Signature. Logs details of the result.

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

Parameters:

log_level (int)

Returns:

signing AP actor id, or None if the request isn’t signed

Return type:

str or None

Raises:

RuntimeError – if the signature is invalid or can’t be verified

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

featured(id)[source]

Serves a user’s AP featured collection for pinned posts.

https://docs.joinmastodon.org/spec/activitypub/#featured

We inline the featured collection in users’ actors, but Mastodon (and Pleroma/Akkoma?) require it to be fetchable separately too. :(

Also, it’s critical that the collection items here are expanded objects! Originally they were compacted string ids, but that triggered a massive flood of requests from Pleroma and Akkoma: https://github.com/snarfed/bridgy-fed/issues/1374#issuecomment-2891993190

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

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

atproto

ATProto protocol implementation.

https://atproto.com/

init(sequences_cls)[source]

Connect arroba’s storage and sequence numbers.

Bridgy Fed uses memcache sequence number allocation in production. If we ever allocated a sequence number from the datastore instead of memcache, we’d allocate a duplicate from memcache and collide. So, we make all services that use this module explicitly initialize it with the sequence number class they need.

https://github.com/snarfed/bridgy-fed/issues/2269

Parameters:

sequences_clsarroba.storage.Sequences subclass

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 RemoteSequences(base_url)[source]

Bases: Sequences

Sequence number implementation that uses remote HTTP endpoints.

Makes requests to /admin/sequences/alloc and /admin/sequences/last to allocate and retrieve sequence numbers.

Used for local shells and scripts, outside GCP.

allocate(nsid)[source]

Allocates a sequence number via HTTP POST.

Parameters:

nsid (str) – subscription XRPC method this sequence number is for

Return type:

int

last(nsid)[source]

Gets the last sequence number via HTTP GET.

Parameters:

nsid (str) – subscription XRPC method this sequence number is for

Return type:

int or None

class DatastoreClient(remote=True, *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.

remote = True
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_EMOJI = '🦋'
LOGO_HTML = '<img src="/oauth_dropins_static/bluesky.svg">'
DEFAULT_TARGET = 'https://atproto.brid.gy'

Note that PDS hostname is atproto.brid.gy here, not bsky.brid.gy. Also note that PDS URLs 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', 'app.bsky.graph.listblock', 'site.standard.document', 'site.standard.publication'})

Which incoming record lexicons we should accept from the firehose.

SUPPORTED_RECORD_TYPES_BETA_USERS = frozenset({})

Which incoming record lexicons to accept from the firehose only for beta users.

STORE_RECORD_TYPES = frozenset({'community.lexicon.payments.webMonetization'})
SUPPORTS_DMS = True
HTML_PROFILES = False
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

  • did (str) – ATProto DID

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

Thin wrapper that converts bsky.app URLs to at:// URIs.

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, multiple=False, strict_json=False, **kwargs)[source]

Converts a models.Object to app.bsky.* lexicon JSON record(s).

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

  • multiple – whether to return multiple records. Default False.

  • strict_json – if True, blob CIDs will be encoded as base32

  • kwargs – passed through to granary.bluesky.from_as1()

Returns:

one or more JSON objects, depending on multiple

Return type:

dict or list of dict

classmethod create_account_for_migrate_out(user, pds, email, password, handle=None, invite_code=None, phone_verification_code=None)[source]

Creates an account on a new PDS that we can migrate out to.

https://atproto.com/guides/account-migration https://github.com/snarfed/bounce/issues/64

Parameters:
  • user (User) – must be already bridged to ATProto

  • pds (str) – new PDS URL, eg https://pds.com

  • email (str)

  • password (str)

  • handle (str) – optional. defaults to generating one based on the user’s native handle and the new PDS’s first available domain

  • invite_code (str) – optional

  • phone_verification_code (str) – optional

Returns:

response from createAccount, including accessJwt,

refreshJwt, handle, did

Return type:

dict

Raises:
classmethod migrate_out(user, to_user_id, to_pds, access_token, refresh_token, handle=None)[source]

Migrates a bridged ATProto account out to a new PDS.

The new PDS must already have an account created for this DID. Use create_account_for_migrate_out() for that.

Deactivates our own repo and disables user bridging to ATProto.

https://atproto.com/guides/account-migration https://github.com/snarfed/bounce/issues/64

Parameters:
  • user (User)

  • to_user_id (str) – DID of the account being migrated

  • to_pds (str) – new PDS URL, eg https://pds.com

  • access_token (str)

  • refresh_token (str)

  • handle (str) – optional. defaults to keeping the existing handle

Raises:

ValueError – eg if ATProto doesn’t own to_user_id

classmethod migrate_out_blobs(user, auth)[source]

Copies a user’s blobs to an external PDS.

Parameters:
  • user (User) – the user being migrated

  • auth (BlueskyAuth) – auth for the new PDS

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

Calls the parent implementation and then truncates the result for Bluesky’s character limits.

Parameters:
  • obj (Object) – user’s actor/profile object

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

generate_rkey(type)[source]

Generates a new rkey based on a collection lexicon’s key type.

https://atproto.com/specs/lexicon#record https://atproto.com/specs/record-key

Parameters:

type (str) – collection NSID, eg app.bsky.feed.post

Returns:

rkey, eg self or 3me5cnh2poyt2

Return type:

str or None

postprocess_writes(writes, user)[source]

Applies custom logic to writes before we commit them.

  • For site.standard.document``s: * Populate the ``bskyPostRef property, if we also have an

    app.bsky.feed.post.

    • Create a site.standard.publication if we don’t already have one.

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

site_standard_publication()[source]

Serves site.standard.publication records for bridged users.

https://standard.site/#verification

Query params:

protocol (str) domain (str): web site domain

bluesky_oauth_client_metadata()[source]

https://docs.bsky.app/docs/advanced-guides/oauth-client#client-and-server-metadata

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.

handler()[source]

Wrapper around handle() that catches exceptions and restarts.

handle(limit=None)[source]
Parameters:

limit – integer (optional): only used in tests

common

Misc common utilities.

bot_user_ids()[source]

Returns all copy ids for protocol bot users.

error(err, status=400, exc_info=None, **kwargs)[source]

Like webutil.flask_util.error(), but wraps body in JSON.

Wrapper around 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 webutil.util.pretty_link()

content_type(resp)[source]

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

create_task(queue, app_id='bridgy-federated', delay=None, app=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)

  • app (Flask) – if not provided, defaults to router.app

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

log_request()[source]

Logs GET query params and POST form.

Limits each value to 1000 chars.

secret_key_auth(fn)[source]

Flask decorator that returns HTTP 401 if the request isn’t authorized.

Right now this only handles internal authorization: the Authorization header has to be set to the Flask secret key in the flask_secret_key file.

Ignored if LOCAL_SERVER is True.

Must be used below flask.Flask.route(), eg:

@app.route(‘/path’) @secret_key_auth def handler():

make_jwt(*, user, scope, expiration=datetime.timedelta(days=7), **claims)[source]

Makes a per-user JWT signed by our EncryptedProperty symmetric key.

Parameters:
  • user (User)

  • scope (str)

  • expiration (timedelta)

  • (str (**claims) – str): optional additional claims

Return type:

str

verify_jwt(token, *, user_id, scope, **claims)[source]

Verifies a per-user JWT and checks that it matches a user, scope, etc.

Raises the appropriate werkzeug HTTPException if the JWT doesn’t verify or match, otherwise returns None.

Parameters:
  • token (str)

  • user_id (str)

  • scope (str)

  • (str (**claims) – str): optional additional claims to check

Raises:
  • Unauthorized – if the token is invalid

  • Forbidden – if the token is valid but for the wrong user or scope

class FlashErrors[source]

Bases: View

Wraps a Flask flask.view.View and flashes errors.

Mostly used with OAuth endpoints.

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.

class CommandSpec(fn: Callable, from_user_bridged: bool | None, to_user_bridged: object, help_text: str | None = None)[source]

Bases: object

from_user_bridged: bool | None

Whether the user sending the DM should be bridged. True, False, or None for either.

to_user_bridged: object

Whether to_user should already be bridged. True, False, None for either, or 'eligible' for not bridged but eligible.

help_text: str | None = None

One-line help string for this command, including name and args, eg '<em>username [domain]</em>: set a custom domain username (handle)'

command(names, *, to_proto=None, from_user_bridged=None, to_user_bridged=None, help_text=None)[source]

Function decorator. Defines and registers a DM command.

The decorated function’s signature determines the cmd_args it accepts. After (from_user, to_proto), required positionals are required cmd_args, defaulted positionals are optional, and *args accepts any number.

If the function declares a to_user parameter, cmd_args[0] is loaded via load_user() and passed in.

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

  • to_proto (str) – if set, only dispatch to this handler when the DM’s recipient protocol has this LABEL. If None, this handler is the generic fallback for any to_proto without a specific handler.

  • from_user_bridged (bool) – whether the user sending the DM should be bridged. True, False, or None for either.

  • to_user_bridged – whether to_user should already be bridged. True, False, None for either, or 'eligible' for not bridged but eligible.

load_user(handle, proto, from_proto, bridged)[source]

Loads the user for handle and applies the bridged policy.

Parameters:
  • handle (str) – the handle or id to look up

  • proto (Protocol) – the protocol the handle belongs to

  • from_proto (Protocol) – the sender’s protocol, used for the bridged enabled check

  • bridged (bool or str) – whether the user should be bridged into from_proto. True, False, None for either, or 'eligible' for not bridged but eligible.

Return type:

User

Raises: ValueError

dispatch(spec, from_user, to_proto, cmd, cmd_args, dm_as1)[source]

Dispatches a parsed DM command to its handler.

Validates cmd_args, optionally loads to_user via load_user(), enforces spec.from_user_bridged, then invokes spec.fn and sends its return value (if any) as a reply.

Parameters:
  • spec (CommandSpec) – the registered command’s spec

  • from_user (User) – the user who sent the DM

  • to_proto (Protocol) – the protocol bot account they sent it to

  • cmd (str or None) – the command name as typed, used in error messages

  • cmd_args (list of str) – the tokens after the command name

  • dm_as1 (dict) – the inbound DM as AS1; id is used as the reply’s inReplyTo

Returns:

a (body, status) tuple suitable for returning from a

Flask view. Always ('OK', 200) once a reply (if any) is sent.

Return type:

(str, int)

maybe_send(*, from_, to_user, text, type=None, in_reply_to=None, **kwargs)[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 (Protocol or User)

  • 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

  • kwargs – added to the outgoing DM activity as additional (AS1) fields

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

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.

NOTE: unlike translate_object_id(), if to is a HAS_COPIES protocol and has no copy object for id, this function returns None, not id!

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.

TODO: what should this return if id is not a valid user id in proto? TODO: add and use new is_user_id function for this ^

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

  • Farcaster: * 123 => farcaster://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

normalize_object_id(*, id, proto)[source]

Normalizes an object id to its canonical representation in a given protocol.

If id is a user id, and this protocol’s profile objects have different ids than their user ids, returns the profile id.

Examples:

Parameters:
Returns:

the normalized object 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, short=False)[source]

Translates a user handle from one protocol to another.

Parameters:
  • handle (str)

  • from (Protocol)

  • to (Protocol)

  • short (bool) – whether to return the full handle or a shortened form. Default False. Currently only affects ActivityPub; returns just @[user] instead of @[user]@[domain]

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.

Allows any id if from_ is UIProtocol or if id is ui:....

NOTE: unlike translate_user_id(), if to is a HAS_COPIES protocol and has no copy object for id, this function returns id, not None!

TODO: unify with translate_user_id().

Parameters:
Returns:

the corresponding id in to

Return type:

str

handle_as_domain(handle)[source]

Converts a handle to domain-like format.

Converts handle to domain format by removing leading @ and replacing @ with ., and replacing certain characters (_ ~ :) with -.

For example: * @user@instance.com => user.instance.com * user_name@instance.com => user-name.instance.com * @alice@inst~test.com => alice.inst-test.com

Parameters:

handle (str or None)

Returns:

if handle is None

Return type:

str or None

memcache

Utilities for caching data in memcache.

TODO: move most or all of this to webutil?

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

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.

evict(entity_key)[source]

Evict a datastore entity from memcache.

For models.User and models.Object entities, also clears their copies from the models.get_original_user_key() and models.get_original_object_key() memoize caches.

Parameters:

entity_key (google.cloud.ndb.Key)

evict_raw(key)[source]

Evict a key from memcache.

Parameters:

key (str)

Returns:

whether the key existed and was deleted

Return type:

bool

remote_evict(entity_key)[source]

Send a request to production Bridgy Fed to evict an entity from memcache.

Parameters:

entity_key (google.cloud.ndb.Key)

Return type:

Response

task_eta(queue, user_id, protocol=None)[source]

Get the ETA to use for a given user’s task in a given queue.

Task rate limit delays are per user, stored in memcache with a key based on queue and user_id and an integer value of POSIX timestamp (UTC) in seconds.

Only generates ETAs for task queues in PER_USER_TASK_RATES. Calls for other queues always return None.

Background: https://github.com/snarfed/bridgy-fed/issues/1788

Parameters:
  • queue (str)

  • user_id (str)

  • protocol (str) – optional protocol label to look up protocol-specific delay and RateLimitType

Returns:

the ETA for this task, or None if the ETA is now

Return type:

datetime

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.

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, the subset for ineligible users):

  • dms_not_supported-[RECIPIENT-USER-ID]

  • moved

  • no-feed-or-webmention

  • no-nip05

  • no-profile

  • opt-out

  • over-handle-domain-limit

  • owns-webfinger

  • private

  • replied_to_bridged_user

  • request_bridging

  • requires-avatar

  • requires-name

  • requires-old-account

  • unsupported-handle-ap

  • welcome

protocol
class KeyPair(**kwargs)[source]

Bases: Model

A user’s public/private key pair for a single protocol.

Used in User.keypairs ; not stored as top-level entities in the datastore. The private key is encrypted at rest; the public key is not.

Format per algorithm:

  • rsa: private_key_bytes is PKCS#1 PEM (-----BEGIN RSA PRIVATE KEY-----); public_key_bytes is SPKI PEM (-----BEGIN PUBLIC KEY-----).

  • secp256k1: 32 raw bytes private, 32 raw bytes BIP-340 x-only public.

  • ed25519: 32 raw bytes private, 32 raw bytes public.

Details for each protocol:

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

Bases: MetaModel

User metaclass. Registers all subclasses in PROTOCOLS.

reset_protocol_properties()[source]

Recreates various protocol properties to include choices from PROTOCOLS.

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)

Returns:

google.cloud.ndb.Key or None

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

Bases: object

Mixin class that defines the add() and remove() methods.

If a subclass of this mixin defines the GET_ORIGINAL_FN class-level attribute, its memoize cache will be cleared when remove() is called with the copies property.

lock = None

Synchronizes add(), remove(), etc.

add(prop, val)[source]

Adds a value to a multiply-valued property.

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.

Parameters:
  • prop (str)

  • val

remove_copies_on(proto)[source]

Removes all copies on a given protocol.

proto.HAS_COPIES must be True.

Parameters:

proto (protocol.Protocol subclass)

class User(**kwargs)[source]

Bases: AddRemoveMixin, StringIdModel

Abstract base class for a Bridgy Fed user.

GET_ORIGINAL_FN()

used by AddRemoveMixin

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

keypairs

Key pairs for this user, one per bridged protocol. Encrypted at rest.

manual_opt_out

Set to True to manually disable this user. Set to False to override spam filters and forcibly enable this user.

enabled_protocols

Protocols that this user has explicitly opted into.

Protocols that don’t require explicit opt in are omitted here.

has_object_feed_followers_on

Protocol labels of protocols that use USES_OBJECT_FEED and have ever had a follower of this user.

sent_dms

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

send_notifs

Which notifications we should send this user.

blocks
verified_domain

Domain that we’ve verified this user owns, eg web site top-level NIP-05, etc.

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

Try to prevent instantiation. Use subclasses instead.

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

Loads and returns a User. Creates it if necessary.

If allow_opt_out is False and id is the bridged id for a user in another protocol, returns that user instead. Note that they’ll be a different type than cls!

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

  • raise (bool) – passed through to User.reload_profile(). If False, and User.reload_profile() returns None when fetching the user’s profile, this method raises RuntimeError

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

status_description()[source]

Returns a human-readable description of this user’s status.

…or None if this user’s status is None, or a description isn’t available.

Returns:

str

is_enabled(to_proto, explicit=False)[source]

Returns True if this user is 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, short=False)[source]

Returns this user’s handle in a different protocol.

Parameters:
  • to_proto (str or Protocol)

  • short (bool) – whether to return the full handle or a shortened form. Default False. Currently only affects ActivityPub; returns just @[user] instead of @[user]@[domain]

Return type:

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]

Returns the user’s PEM-encoded ActivityPub public RSA key.

Return type:

bytes

private_pem()[source]

Returns the user’s PEM-encoded ActivityPub private RSA key.

Return type:

bytes

nsec()[source]

Returns the user’s bech32-encoded Nostr private secp256k1 key.

Return type:

str

hex_pubkey()[source]

Returns the user’s hex-encoded Nostr public secp256k1 key.

Return type:

str

npub()[source]

Returns the user’s bech32-encoded ActivityPub public secp256k1 key.

Return type:

str

farcaster_key()[source]

Returns the user’s Farcaster signing key.

TODO: real per-user signer keys, registered on-chain via the KeyRegistry. Messages signed with these stub keys will be rejected by the hub.

Return type:

cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey

name()[source]

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

web_url()[source]

Returns this user’s user-facing profile page URL.

…eg https://bsky.app/profile/snarfed.org or 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

is_profile(obj)[source]

Returns True if obj is this user’s profile/actor, False otherwise.

Parameters:

obj (Object)

Return type:

bool

reload_profile(raise_=False, **kwargs)[source]

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

Populates the reloaded profile Object in self.obj.

Parameters:
  • raise (bool) – passed through to Protocol.load(). If False, and Protocol.load() returns None when fetching the user’s profile, this method raises RuntimeError

  • kwargs – passed through to Protocol.load()

Raises:

RuntimeError – if the user’s profile can’t be loaded

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) – True to include handle, False to exclude it, 'short' to include a shortened version, if available

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

  • logo (str) – optional path to platform logo to show instead of the protocol’s default

  • 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

is_blocking(user_or_id)[source]

Returns True if this user is is blocking user_or_id, False otherwise.

Looks at domain blocklists in blocks. Eventually we can add support for blocking individual users in that too.

Parameters:

user_or_id (User or str)

Return type:

bool

add_domain_blocklist(url)[source]

Adds a domain blocklist to this user.

Loads the CSV at the given URL adds it to blocks if it’s not already there.

Parameters:

url (str) – URL of CSV blocklist to add

Returns:

CSV blocklist, or None if it couldn’t be loaded

Return type:

Object

remove_domain_blocklist(url)[source]

Removes a domain blocklist from this user.

Parameters:

url (str) – URL of CSV blocklist to remove

Returns:

CSV blocklist, or None if it couldn’t be loaded

Return type:

Object

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

Bases: AddRemoveMixin, StringIdModel

An activity or other object, eg actor.

Key name is the id, generally a URI. We synthesize ids if necessary.

GET_ORIGINAL_FN()

used by AddRemoveMixin

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.

TODO: nail down whether this is ABBREV` or LABEL

as2

ActivityStreams 2, for ActivityPub

bsky

AT Protocol lexicon, for Bluesky

csv

Other standalone CSV data, eg domain blocklist.

farcaster

List of binary serialized Farcaster Message protobufs.

Each blob is SerializeToString() and decodes via Message.FromString(blob). Repeated to support actors as multiple USER_DATA_ADD messages.

mf2

HTML microformats2 item (not top level parse object with items field)

nostr

Nostr event

our_as1

ActivityStreams 1, for activities that we generate or modify ourselves

raw

Other standalone data format, eg DID document

extra_as1

Additional individual fields to merge into this object’s AS1 representation

is_csv

Whether this object is a CSV. Needed because csv isn’t indexed.

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.

Returns an HTML link to this object’s user-facing web URL, if any.

Return type:

str or None

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

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

TODO: for some protocols, we should try harder to find the right copy id. Eg if if copies has some old garbage entries for this protocol, and we can tell that they don’t belong to the user’s copy account in this protocol, eg if the DID in the at:// URI doesn’t match, we should skip those and look for the matching copy. We’d need the user here though. This would help with or fix: https://console.cloud.google.com/errors/detail/COK22a6w4O2JVg;locations=global;time=P30D?project=bridgy-federated

Parameters:

protoProtocol subclass

Return type:

str

get_copies(proto)[source]

Returns all ids of copies of this object in a given protocol.

If proto is source_protocol, returns this object’s key id.

Parameters:

protoProtocol subclass

Return type:

list of str

resolve_ids()[source]

Replaces “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

  • attachments.[objectType=note].id

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

owner_protocol()[source]

Wrapper around source_protocol that handles UIProtocol.

Returns:

source_protocol unless it’s None or

UIProtocol, in which case infers and returns author’s or actor’s protocol instead.

Return type:

Protocol subclass

property domain_blocklist

Returns the domains in the domain blocklist in raw or csv.

If raw is a list, returns it directly. Otherwise extracts the ‘domain’ or ‘#domain’ column from csv.

TODO: unify with filters.blocklist_items()

Returns:

domain names, or empty list if neither raw nor

csv is populated or parseable.

Return type:

list of str

domain_blocklist_matches(user_or_id)[source]

Returns True if user_or_id is in this domain blocklist, False otherwise.

For users, looks at id, handle, and delivery target.

Parameters:

user_or_id (User or str)

Return type:

bool

Raises:

AssertionError – if this object is not a domain blocklist

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

dormant means the followee isn’t bridged (yet), so the follow can’t be delivered. If they enable the bridge, we notify the follower.

reason

Optional explanation for this follow’s status, eg why it’s dormant. One of REASONS.

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.

If status='dormant' is passed and the existing Follower is active, the existing Follower is returned unchanged: we never downgrade an active Follower to dormant.

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, max_age=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

hydrate(activity, fields=('author', 'actor', 'object'))[source]

Hydrates fields in an AS1 activity, in place.

Parameters:
  • activity (dict) – AS1 activity

  • fields (sequence of str) – names of fields to hydrate. If they’re string ids, loads them from the datastore, if possible, and replaces them with their dict AS1 objects.

Returns:

tasklets for hydrating

each field. Wait on these before using activity.

Return type:

sequence of google.cloud.ndb.tasklets.Future

fetch_page(query, model_class, by=None, max_age=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:
  • query (Query)

  • model_class (class)

  • by (ndb.model.Property) – paging property, eg Object.updated or Object.created

  • max_age (timedelta) – if provided, reject before/after params older than this, and don’t generate paging links past it

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

load_user(handle_or_id, proto=None, create=False, allow_opt_out=False, raise_=False)[source]

Loads a user by handle or id.

Parameters:
  • handle_or_id (str) – user handle or id

  • proto (Protocol subclass or None) – protocol to use. If None, will try to determine protocol via Protocol.for_id and Protocol.for_handle

  • create (bool) – if True, use get_or_create; if False, use get_by_id

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

  • raise (bool) – passed through to User.reload_profile(). If False, and User.reload_profile() returns None when fetching the user’s profile, this method raises RuntimeError

Return type:

User

Raises:

RuntimeError – if no matching user was found

maybe_truncate_key_id(id)[source]

Returns id, truncated to _MAX_KEYPART_BYTES bytes if it’s longer.

nostr

Nostr protocol implementation.

https://github.com/nostr-protocol/nostr https://github.com/nostr-protocol/nips/blob/master/01.md https://github.com/nostr-protocol/nips#list

Nostr Object key ids are NIP-21 nostr:… URIs. https://nips.nostr.com/21

class NostrRelay(**kwargs)[source]

Bases: StringIdModel

The last created_at we’ve seen from a given relay.

Key id is full relay URI, eg wss://nos.lol. Used in nostr_hub.

https://nips.nostr.com/1#from-client-to-relay-sending-events-and-creating-subscriptions

since
created
updated
class Nostr(**kwargs)[source]

Bases: User, Protocol

Nostr class.

Key id is hex pubkey with nostr: prefix.

relays

NIP-65 kind 10002 event with this user’s relays.

valid_nip05

NIP-05 identifier that we’ve resolved and verified.

hex_pubkey()[source]

Returns the user’s hex-encoded Nostr public secp256k1 key.

Return type:

str

npub()[source]

Returns the user’s bech32-encoded ActivityPub public secp256k1 key.

Return type:

str

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

Returns the first NIP-65 relay for the given object’s author.

classmethod check_supported(obj, direction)[source]

Update is only supported for actors and articles, not notes.

classmethod create_for(user)[source]

Creates a Nostr profile for a non-Nostr user.

Parameters:

user (User)

reload_profile(**kwargs)[source]

Reloads this user’s kind 0 profile, NIP-65 relay list, and NIP-05 id.

https://nips.nostr.com/1#kinds https://nips.nostr.com/65 https://nips.nostr.com/5

classmethod set_username(user, username)[source]

check NIP-05 DNS, then update profile event with nip05?

classmethod fetch(obj, **kwargs)[source]

Fetches a Nostr event from a relay.

Parameters:
  • obj (Object) – with the id to fetch. Fills data into the nostr property.

  • kwargs – ignored

Returns:

True if the object was fetched and populated successfully,

False otherwise

Return type:

bool

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

Converts a models.Object to a Nostr event.

Parameters:
  • obj (Object)

  • from_user (User) – user this object is from

  • kwargs – unused

Returns:

JSON Nostr event

Return type:

dict

classmethod send(obj, relay_url, from_user=None, **kwargs)[source]

Sends an event to a relay.

Events are immutable, so all operations happen by sending a new event, including updates and deletes. granary.nostr.from_as1() translates all of those, so all we have to do here is convert and send the event.

nip_05()[source]

NIP-05 endpoint that serves handles for users bridged into Nostr.

https://nips.nostr.com/5

Query params:

name (str): should only contain a-z0-9-_.

Returns a JSON object with:

names: {<name>: <pubkey hex>} relays: optional, {<pubkey hex>: [relay urls]}

normalize_relay_uri(uri)[source]

Returns a normalized relay URI.

Right now, just adds a trailing slash if the URI has no path, and removes the port if it’s explicitly provided and redundant, ie :443 for wss:// or :80 for ws://.

https://github.com/nostr-protocol/nips/issues/1876 https://github.com/nostr-protocol/nips/issues/1198

Parameters:

uri (str)

Returns:

normalized URI

Return type:

str

notifications

Send DM notifications of replies, quote posts, mentions from unbridged users.

add_notification(user, obj)[source]

Adds a notification for a given user.

The memcache key is notifs-{user id}. The value is a space-separated list of object ids to notify the user of.

Uses gets/cas to create the cache entry if it doesn’t exist.

Parameters:
  • user (User) – the user to notify

  • obj (Object) – the object to notify about

get_notifications(user, clear=False)[source]

Gets enqueued notifications for a given user.

The memcache key is notifs-{user id}.

Parameters:
  • user (User)

  • clear (bool) – clear notifications from memcache after fetching them

Returns:

Object ids to notify the user of; possibly empty

Return type:

list of str

notify_task()[source]

Task handler for sending a notification DM to a user.

Fetches notifications from memcache.

Parameters:
  • user_id (str) – ID of the user to send notifications to

  • protocol (str) – Protocol label the user is on

pages

UI pages.

load_user(proto, id)[source]

Loads and returns the current request’s user.

Parameters:
Return type:

User

Raises:

HTTPException

no_paging(fn)[source]

Returns 400 if the request has before or after params.

require_login(fn)[source]

Decorator that requires and loads the current request’s logged in user.

Passes the user in the user kwarg, as a models.User.

HTTP POST params:

key (str): url-safe ndb key

Raises:

HTTPException

require_token(scope, claims=None)[source]

Decorator that loads a user and checks that they’re authorized by token.

Expects a user_id kwarg. Loads the matching user and replaces it with a user kwarg passed to the handler with the loaded User.

The per-user JWT should be in the token query param or form arg.

Must be used below flask.Flask.route(), eg:

@app.route(‘/path’) @require_token(‘respond’) def handler():

Parameters:
  • scope (str) – expected scope that the JWT must match

  • claims (sequence of str) – optional additional claims that the JWT must match

get_logins()[source]

Returns the user’s current logged in sessions:

Returns:

list of oauth_dropins.models.BaseAuth

login_to_user_key(login)[source]

“Converts an oauth-dropins auth entity to a :model:`User` key.

Parameters:

login (BaseAuth)

Return type:

ndb.key.Key

render(template, **vars)[source]

Renders a Jinja2 template and adds our standard template variables.

Parameters:

template (str) – file name

front_page()[source]

View for the front page.

docs()[source]

View for the docs page.

login()[source]

View for the front page.

logout()[source]

Logs the user out of all current login sessions.

settings()[source]

User settings page. Requires logged in session.

enable(user=None)[source]

Enables bridging for a given account.

Parameters:

user (User)

disable(user=None)[source]

Disables bridging for a given account.

Parameters:

user (User)

set_username(user=None)[source]

Enables bridging for a given account.

Parameters:

user (User)

Query params:

protocol (str) username (str)

block(user=None)[source]

Blocks a user or blocklist.

TODO: unify with unblock()?

Parameters:

user (User) – current logged in user, provided by require_login()

Query params:

target (str)

unblock(user=None)[source]

Unblocks a user or blocklist.

TODO: unify with block()?

Parameters:

user (User) – current logged in user, provided by require_login()

Query params:

target (str)

toggle_notifs(user=None)[source]

Toggles DM notifications for a given account.

Parameters:

user (User)

migrate_to_activitypub(user=None)[source]

Migrates a bridged account out to a native fediverse account.

Duplicates dms.migrate_to_activitypub() and Bounce’s confirm and migrate_out. Keep them in sync!

Parameters:

user (User)

Form params:

handle (str): the destination fediverse handle or id

migrate_to_atproto(user=None)[source]

First step of migrating a bridged account out to a new ATProto PDS.

Calls describeServer on the new PDS, then shows the create account form.

Duplicates dms.migrate_to_atproto() and Bounce’s confirm and migrate_out. Keep them in sync!

Parameters:

user (User)

Form params:

pds (str): the new PDS’s URL

migrate_to_atproto_phone_verification(user=None)[source]

Requests a phone verification SMS from the new ATProto PDS.

Duplicates dms.migrate_to_atproto() and Bounce’s bluesky_phone_verification_post. Keep them in sync!

Parameters:

user (User)

Form params:

pds (str): the new PDS’s URL phone_number (str)

migrate_to_atproto_create_account(user=None)[source]

Final step of migrating a bridged account out to a new ATProto PDS.

Creates the account on the new PDS, then migrates the bridged account out.

Parameters:

user (User)

Form params:

pds (str): the new PDS’s URL email (str) password (str) handle (str): optional, the handle’s first segment, eg alice handle_domain (str): optional, the new PDS’s handle domain, eg .pds.com invite_code (str): optional phone_verification_code (str): optional

feed(protocol, id)[source]
Query params:

format: html, atom, or rss

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

respond(user)[source]

Lets a user reply to, like, or repost an unbridged post.

Query params:

obj_id (str): Object id token (str): JWT token for user authentication

respond_reply(user)[source]

Creates a reply activity.

Form params:

obj_id (str): Object id to reply to content (str): reply text content token (str): JWT token for user authentication

respond_like(user)[source]

Creates a like activity.

Form params:

obj_id (str): Object id to like token (str): JWT token for user authentication

respond_repost(user)[source]

Creates a repost/share activity.

Form params:

obj_id (str): Object id to repost token (str): JWT token for user authentication

respond_block(user)[source]

Creates a block activity.

Form params:

obj_id (str): Object id to get the actor from to block token (str): JWT token for user authentication

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_EMOJI = ''

logo emoji, if any

Type:

str

LOGO_HTML = ''

logo <img> tag, if any

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

DEFAULT_TARGET = None

optional, the default target URI to send this protocol’s activities to. May be used as the “shared” target. Often only set if HAS_COPIES is true.

Type:

str

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

USES_OBJECT_FEED = False

whether to store followers on this protocol in Object.feed.

Type:

bool

HTML_PROFILES = False

whether this protocol supports HTML in profile descriptions. If False, profile descriptions should be plain text.

Type:

bool

SEND_REPLIES_TO_ORIG_POSTS_MENTIONS = False

whether replies to this protocol should include the original post’s mentions as delivery targets

Type:

bool

BOTS_FOLLOW_BACK = False

when a user on this protocol follows a bot user to enable bridging, does the bot follow them back?

Type:

bool

HANDLES_PER_PAY_LEVEL_DOMAIN = None

how many users to allow with handles on the same pay-level domain. None for no limit.

Type:

int

RECEIVE_FILTERS = ()

filter functions from filters.py to apply to incoming activities. Applied in order, so put the cheapest filters first.

Type:

tuple of callable

RATE_LIMIT_TYPE = 1

Whether receive and send task rate limiting increases linearly or exponential.

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 or objects, 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 and DIDs. 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 domains.DOMAIN_BLOCKLIST.

Parameters:

id (str) – user id or object id

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

Returns the authenticated user id for the current request.

Checks authentication on the current request, eg HTTP Signature for ActivityPub. To be implemented by subclasses.

Returns:

authenticated user id, or None if there is no authentication

Return type:

str

Raises:
  • RuntimeError – if the request’s authentication (eg signature) is

  • invalid or otherwise can't be verified

classmethod key_for(id, allow_opt_out=False)[source]

Returns the google.cloud.ndb.Key for a given id’s models.User.

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 is_user_at_domain(handle, allow_internal=False)[source]

Returns True if handle is formatted user@domain.tld, False otherwise.

Example: @user@instance.com

Parameters:
  • handle (str)

  • allow_internal (bool) – whether the domain can be a Bridgy Fed domain

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, target, from_user=None, orig_obj_id=None)[source]

Sends an outgoing activity.

To be implemented by subclasses. Should call to_cls.translate_ids(obj.as1) before converting it to this Protocol’s format.

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, and store it back in the datastore, in a transaction!

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

  • target (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, werkzeug.HTTPException,

  • websockets.WebSocketException, etc – 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, or None

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” to the user’s actor’s summary.

Uses HTML for protocols that support it, plain text otherwise.

Parameters:
  • cls (Protocol subclass) – protocol that the user is bridging into

  • obj (Object) – user’s actor/profile 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 check_can_migrate_out(user, to_user_id)[source]

Raises an exception if a user can’t yet migrate to a native account.

For example, if to_user_id isn’t on this protocol, or if user is on this protocol, or isn’t bridged to this protocol.

If the user is ready to migrate, returns None.

Subclasses may override this to add more criteria, but they should call this implementation first.

Parameters:
Raises:

ValueError – if user isn’t ready to migrate to this protocol yet

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

Migrates a native account in to be a bridged account.

The protocol independent parts are done here; protocol-specific parts are done in _migrate_in(), which this wraps.

Reloads the user’s profile before calling _migrate_in().

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

  • featured[].items, featured[].orderedItems

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

translated AS1 version of obj

Return type:

dict

classmethod translate_mention_handles(obj)[source]

Translates @-mentions in obj.content to this protocol’s handles.

Specifically, for each mention tag in the object’s tags that has startIndex and length, replaces it in obj.content with that user’s translated handle in this protocol and updates the tag’s location.

Called by Protocol.translate_ids().

If obj.content is HTML, does nothing.

Parameters:

obj (dict) – AS1 object

Returns:

modified AS1 object

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, from_user)[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_maybe_follow_back(user)[source]

Follow a user from a protocol bot user, if their protocol needs that.

…so that the protocol starts sending us their activities, if it needs a follow for that (eg ActivityPub).

Parameters:

user (User)

classmethod handle_move(obj, from_user)[source]

Handles an incoming move (account migration) activity.

Updates all of the account’s :class:`Follower`s to point to the new id.

Parameters:
  • obj (Object) – follow activity

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

classmethod handle_bare_object(obj, *, authed_as, from_user)[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

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

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

Return type:

dict

classmethod load(id, remote=None, local=True, raise_=True, raw=False, csv=False, **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

  • raw (bool) – whether to load this as a “raw” id, as is, without normalizing to an on-protocol object id. Exact meaning varies by subclass.

  • csv (bool) – whether to specifically load a CSV object TODO: merge this into raw, using returned Content-Type?

  • 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, direction)[source]

If this protocol doesn’t support this activity, 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)

  • direction (str) – 'receive' or 'send'

Raises:

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

classmethod block(from_user, arg)[source]

Blocks a user or list.

Parameters:
  • from_user (User) – user doing the blocking

  • arg (str) – handle or id of user/list to block

Returns:

user or list that was blocked

Return type:

User or Object

Raises:

ValueError – if arg doesn’t look like a user or list on this protocol

classmethod unblock(from_user, arg)[source]

Unblocks a user or list.

Parameters:
  • from_user (User) – user doing the unblocking

  • arg (str) – handle or id of user/list to unblock

Returns:

user or list that was unblocked

Return type:

User or Object

Raises:

ValueError – if arg doesn’t look like a user or list on this protocol

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

user_enabled_task()[source]

Task handler for when a user enables a protocol.

DMs any dormant models.Followers pointing at the user to let them know the user is now bridged, so they can follow them for real, and flips those Followers from dormant to inactive.

Parameters:
  • user (url-safe google.cloud.ndb.key.Key) – the models.User who enabled bridging

  • protocol (str) – LABEL of the protocol they enabled

migrate_out_task()[source]

Task handler for finishing a migration out.

Currently, for migrating out to ATProto, uploads the user’s blobs to the new PDS. Otherwise, does nothing.

Parameters:
  • user (str, url-safe ndb.Key of a User) – the bridged models.User migrating out

  • protocol (str) – destination protocol

  • auth (optional url-safe ndb.Key of an oauth-dropins auth entity) – the user’s new account. For ATProto, an oauth_dropins.bluesky.BlueskyAuth.

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_EMOJI = '🌐'
CONTENT_TYPE = 'text/html; charset=utf-8'
DEFAULT_ENABLED_PROTOCOLS = ('activitypub',)
DEFAULT_SERVE_USER_PAGES = True
SUPPORTED_AS1_TYPES = ('person', 'organization', 'application', 'group', 'service', 'note', 'article', 'link', 'mention', 'comment', 'update', 'post', 'delete', 'undo', 'audio', 'bookmark', 'event', 'image', 'video', 'follow', 'like', 'share', 'stop-following')
USES_OBJECT_FEED = True
HTML_PROFILES = True
has_redirects
redirects_error
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, short=False)[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, target, from_user=None, orig_obj_id=None, **kwargs)[source]

Sends a webmention to a given webmention 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, csv=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:
  • obj (Object)

  • gateway (bool) – passed through to 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

  • csv (bool) – if True, fetch CSV instead of microformatted HTML

  • kwargs – ignored

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

Converts a Object to HTML.

Parameters:
  • obj (Object)

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

  • kwargs – unused

Return type:

str

webmention_external()[source]

Handles inbound webmentions, enqueues tasks to process.

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.

Allows source URLs on brid.gy subdomains if the Authorization header matches the Flask secret key.

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 cache around webutil.webmention.discover().

reload_csvs()[source]

Reloads all CSV :class:`models.Object`s.

Queries for all Objects with is_csv True and reloads each one from its original URL.

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 found

Return type:

str