Moderation
FSharp.ATProto provides convenience functions for muting users and threads, subscribing to moderation lists, and filing reports. All actions are server-side and persist across devices.
All examples assume you have an authenticated agent:
open FSharp.ATProto.Core
open FSharp.ATProto.Bluesky
open FSharp.ATProto.Syntax
Muting a User
Bluesky.muteUser accepts a Profile, ProfileSummary, or Did directly. Muted users' posts are hidden from your feeds and notifications, but the muted user is never notified:
taskResult {
let! profile = Bluesky.getProfile agent someHandle
// Pass the profile directly -- no need to extract .Did
do! Bluesky.muteUser agent profile
// later...
do! Bluesky.unmuteUser agent profile
}
When you only have a handle string, use muteUserByHandle -- it resolves the identifier for you:
do! Bluesky.muteUserByHandle agent "spammer.bsky.social"
Muting is invisible. The muted user can still see and interact with your posts -- you just will not see theirs.
Muting a Thread
Bluesky.muteThread accepts a TimelinePost, PostRef, or AtUri. Posts in the muted thread are hidden from your notifications:
taskResult {
// Pass the post directly -- no need to extract .Uri
do! Bluesky.muteThread agent post
// later...
do! Bluesky.unmuteThread agent post
}
Useful for silencing noisy threads you have been mentioned in. Thread muting only affects notifications -- the thread remains visible if you navigate to it.
Moderation Lists
Bluesky supports community-maintained moderation lists. You can subscribe to a list to mute or block every account on it.
Mute lists
Subscribing to a mute list mutes all accounts on it. As the list owner updates membership, the effect follows automatically:
taskResult {
do! Bluesky.muteModList agent listUri
// later...
do! Bluesky.unmuteModList agent listUri
}
Block lists
blockModList creates a listblock record and returns a ListBlockRef you pass to unblockModList to undo:
taskResult {
let! blockRef = Bluesky.blockModList agent listUri
// later...
do! Bluesky.unblockModList agent blockRef
}
Block lists apply the same restrictions as individual blocks.
Reporting Content
Bluesky.reportContent takes a ReportSubject (what to report), a ReasonType (why), and an optional description. On success it returns the report ID:
taskResult {
// Report a post
let! reportId =
Bluesky.reportContent agent
(ReportSubject.Record postRef)
ComAtprotoModeration.Defs.ReasonType.ReasonSpam
(Some "This is spam content")
printfn "Report filed (ID: %d)" reportId
}
ReportSubject has two cases:
[<RequireQualifiedAccess>]
type ReportSubject =
| Account of Did // report an entire account
| Record of PostRef // report a specific post
To report an account instead of a post:
taskResult {
let! reportId =
Bluesky.reportContent agent
(ReportSubject.Account userDid)
ComAtprotoModeration.Defs.ReasonType.ReasonViolation
None
printfn "Report filed (ID: %d)" reportId
}
Reason types
The ComAtprotoModeration.Defs.ReasonType DU includes these common cases:
Case |
When to use |
|---|---|
|
Unsolicited or repetitive content |
|
Terms of service or community guideline violation |
|
Deceptive or misleading content |
|
Unwanted sexual content |
|
Rude or disrespectful behavior |
|
Does not fit other categories (provide a description) |
|
Appealing a previous moderation decision |
The DU also includes finer-grained Ozone reason types (e.g. ReasonHarassmentTargeted, ReasonMisleadingScam) and an Unknown of string fallback for forward compatibility.
Checking Mute and Block Status
The Profile domain type returned by Bluesky.getProfile includes fields that reflect your current moderation state:
IsMuted-- whether you have muted this userIsBlocking-- whether you are blocking this userIsBlockedBy-- whether this user is blocking you
taskResult {
let! profile = Bluesky.getProfile agent someHandle
if profile.IsMuted then printfn "You have muted this user"
if profile.IsBlocking then printfn "You are blocking this user"
if profile.IsBlockedBy then printfn "This user is blocking you"
}
See the Profiles guide for more on the Profile domain type.
Power Users: Raw XRPC
If you need access to response fields the convenience layer does not expose, drop to the raw XRPC call:
taskResult {
let! output =
ComAtprotoModeration.CreateReport.call agent
{ Subject =
ComAtprotoModeration.CreateReport.InputSubjectUnion.RepoRef
{ Did = userDid }
ReasonType = ComAtprotoModeration.Defs.ReasonType.ReasonSpam
Reason = Some "Detailed description here"
ModTool = None }
printfn "Report %d filed at %A" output.Id output.CreatedAt
}
This gives access to the full response, including CreatedAt, ReportedBy, and the resolved Subject union.
Moderation Engine
The FSharp.ATProto.Moderation package provides a label-aware moderation engine that computes context-specific decisions based on user preferences, labels, muted words, and block state.
Overview
The engine takes moderation preferences, labels on content/accounts, and context (e.g., "is this being shown in a list or a full view?"), and returns a ModerationDecision that tells you what to do: blur, alert, filter, or show normally.
Key Types
Type |
Description |
|---|---|
|
User's label visibility settings, muted words, hidden posts, adult content preference |
|
Computed decision with prioritized causes |
|
What to do: |
|
Where the content appears: |
|
A label applied to content: value, source DID, negation flag |
|
Built-in label behavior definition |
|
Custom label definition from a labeler |
Usage
open FSharp.ATProto.Moderation
// Set up preferences
let prefs : ModerationPrefs =
{ AdultContentEnabled = false
Labels = Map.ofList [ "nsfw", LabelVisibility.Warn ]
MutedWords = []
HiddenPosts = [] }
// Labels on a post
let labels : Label list =
[ { Src = Did.value labelerDid; Uri = AtUri.value postUri; Val = "nsfw"; Neg = false; Cts = None } ]
// Get moderation decision for a content list context
let decision = Moderation.moderatePost prefs labels "" [] [] false false None ModerationContext.ContentList
// Check the primary action
match decision.Action with
| ModerationAction.Filter -> printfn "Hide from feed"
| ModerationAction.Blur -> printfn "Blur this content"
| ModerationAction.Alert -> printfn "Show with warning"
| ModerationAction.Inform -> printfn "Show informational indicator"
| ModerationAction.NoAction -> printfn "Show normally"
Built-in Labels
The engine includes 8 built-in label definitions: porn, sexual, nudity, graphic-media, gore, nsfl, !hide, !warn. Custom labels from labeler services are supported via Labels.interpretLabelValueDefinition.
Functions
Function |
Description |
|---|---|
|
Compute decision for a post (labels + muted words + hidden posts) |
|
Compute decision for a profile |
|
Compute decision for a notification |
|
Compute decision for a feed generator |
|
Compute decision for a user list |
|
General-purpose moderation (labels + mute/block state + target type) |
|
Look up a built-in label definition |
|
Convert a labeler's custom label to a |
namespace FSharp
--------------------
namespace Microsoft.FSharp
module AtpAgent from FSharp.ATProto.Core
<summary> Functions for creating and authenticating <see cref="AtpAgent" /> instances. </summary>
--------------------
type AtpAgent = { HttpClient: HttpClient mutable BaseUrl: Uri mutable Session: AtpSession option ExtraHeaders: (string * string) list AuthenticateRequest: (HttpRequestMessage -> unit) option RefreshAuthentication: (unit -> Task<Result<unit,XrpcError>>) option OnSessionChanged: (unit -> unit) option }
<summary> Client agent for communicating with an AT Protocol Personal Data Server (PDS). Holds the HTTP client, base URL, optional authenticated session, and extra headers. </summary>
<remarks> Create an agent with <see cref="AtpAgent.create" /> or <see cref="AtpAgent.createWithClient" />, then authenticate with <see cref="AtpAgent.login" />. The agent's <see cref="Session" /> field is mutable: it is updated automatically on login and token refresh. </remarks>
<example><code> let agent = AtpAgent.create "https://bsky.social" let! session = AtpAgent.login "my-handle.bsky.social" "app-password-here" agent </code></example>
module TimelinePost from FSharp.ATProto.Bluesky
--------------------
type TimelinePost = { Uri: AtUri Cid: Cid Author: ProfileSummary Text: string Facets: Facet list LikeCount: int64 RepostCount: int64 ReplyCount: int64 QuoteCount: int64 IndexedAt: DateTimeOffset ... }
<summary> A post with engagement counts and viewer state, used in feeds and timelines. Maps from <c>PostView</c>. </summary>
<summary> A reference to a specific version of a post record. Contains both the AT-URI (identifying the record) and the CID (identifying the exact version). Accepted by <c>like</c>, <c>repost</c>, and <c>replyTo</c>. </summary>
module Handle from FSharp.ATProto.Syntax
<summary> Functions for creating, validating, and extracting data from <see cref="Handle" /> values. </summary>
--------------------
type Handle = private | Handle of string override ToString: unit -> string
<summary> A handle (domain name) used as a human-readable identifier in the AT Protocol. Handles are DNS-based names (e.g. <c>my-handle.bsky.social</c>) that resolve to a <see cref="Did" />. They must be valid domain names with at least two segments and a maximum length of 253 characters. </summary>
<remarks> See the AT Protocol specification: https://atproto.com/specs/handle </remarks>
module AtUri from FSharp.ATProto.Syntax
<summary> Functions for creating, validating, and extracting data from <see cref="AtUri" /> values. </summary>
--------------------
type AtUri = private | AtUri of string override ToString: unit -> string
<summary> An AT-URI that identifies a resource in the AT Protocol network. AT-URIs use the scheme <c>at://</c> followed by an authority (DID or handle), an optional collection (NSID), and an optional record key. Format: <c>at://<authority>[/<collection>[/<rkey>]]</c>. Maximum length is 8192 characters. </summary>
<remarks> See the AT Protocol specification: https://atproto.com/specs/at-uri-scheme </remarks>
module Did from FSharp.ATProto.Syntax
<summary> Functions for creating, validating, and extracting data from <see cref="Did" /> values. </summary>
--------------------
type Did = private | Did of string override ToString: unit -> string
<summary> A decentralized identifier (DID) as defined by the AT Protocol. DIDs are the primary stable identifier for accounts. Two methods are currently supported: <c>did:plc:</c> (hosted, managed by PLC directory) and <c>did:web:</c> (self-hosted, DNS-based). </summary>
<remarks> See the AT Protocol specification: https://atproto.com/specs/did and the W3C DID specification: https://www.w3.org/TR/did-core/ </remarks>
<summary> High-level convenience methods for common Bluesky operations: posting, replying, liking, reposting, following, blocking, uploading blobs, and deleting records. All methods require an authenticated <see cref="AtpAgent" />. </summary>
<summary> Get a user's profile. Accepts a <see cref="Handle" />, <see cref="Did" />, <see cref="ProfileSummary" />, or <see cref="Profile" />. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="actor">A <see cref="Handle" />, <see cref="Did" />, <see cref="ProfileSummary" />, or <see cref="Profile" />.</param>
<returns>A <see cref="Profile" /> on success, or an <see cref="XrpcError" />.</returns>
<summary> Mute an account. Accepts a <see cref="Did" />, <see cref="ProfileSummary" />, or <see cref="Profile" /> directly. Muted accounts are hidden from your feeds but not blocked. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="target">The user to mute — a <see cref="Did" />, <see cref="ProfileSummary" />, or <see cref="Profile" />.</param>
<returns><c>unit</c> on success, or an <see cref="XrpcError" />.</returns>
<summary> Unmute a previously muted account. Accepts a <see cref="Did" />, <see cref="ProfileSummary" />, or <see cref="Profile" /> directly. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="target">The user to unmute — a <see cref="Did" />, <see cref="ProfileSummary" />, or <see cref="Profile" />.</param>
<returns><c>unit</c> on success, or an <see cref="XrpcError" />.</returns>
<summary> Mute an account by handle string. The handle is resolved to a DID, then the mute is created. Also accepts a DID string directly (if it starts with <c>did:</c>, it is parsed as a DID). </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="identifier">A handle (e.g., <c>my-handle.bsky.social</c>) or DID string (e.g., <c>did:plc:abc123</c>).</param>
<returns><c>unit</c> on success, or an <see cref="XrpcError" />.</returns>
<remarks> For type-safe usage when you already have a <see cref="Did" />, use <see cref="muteUser" /> instead. </remarks>
<summary> Mute a thread. Posts in the muted thread are hidden from your notifications. Accepts an <see cref="AtUri" />, <see cref="PostRef" />, or <see cref="TimelinePost" />. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="root">The thread root post (an <see cref="AtUri" />, <see cref="PostRef" />, or <see cref="TimelinePost" />).</param>
<returns><c>unit</c> on success, or an <see cref="XrpcError" />.</returns>
<summary> Unmute a previously muted thread. Accepts an <see cref="AtUri" />, <see cref="PostRef" />, or <see cref="TimelinePost" />. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="root">The thread root post (an <see cref="AtUri" />, <see cref="PostRef" />, or <see cref="TimelinePost" />).</param>
<returns><c>unit</c> on success, or an <see cref="XrpcError" />.</returns>
<summary> Mute an entire moderation list. All accounts on the list are muted. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="listUri">The AT-URI of the moderation list to mute.</param>
<returns><c>unit</c> on success, or an <see cref="XrpcError" />.</returns>
<summary> Unmute a previously muted moderation list. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="listUri">The AT-URI of the moderation list to unmute.</param>
<returns><c>unit</c> on success, or an <see cref="XrpcError" />.</returns>
<summary> Block an entire moderation list. Creates a <c>app.bsky.graph.listblock</c> record. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="listUri">The AT-URI of the moderation list to block.</param>
<returns>A <see cref="ListBlockRef" /> on success, or an <see cref="XrpcError" />. Pass the <c>ListBlockRef</c> to <see cref="unblockModList" /> to undo.</returns>
<summary> Unblock a previously blocked moderation list by deleting the list block record. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="listBlockRef">The <see cref="ListBlockRef" /> returned by <see cref="blockModList" />.</param>
<returns><c>Ok ()</c> on success, or an <see cref="XrpcError" />.</returns>
<summary> Report content to moderation. Use <see cref="ReportSubject.Account" /> to report an account or <see cref="ReportSubject.Record" /> to report a specific post. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="subject">The subject of the report (account or post).</param>
<param name="reason">The reason type from <see cref="ComAtprotoModeration.Defs.ReasonType" />.</param>
<param name="description">An optional free-text description of the report.</param>
<returns>The report ID on success, or an <see cref="XrpcError" />.</returns>
<summary>The subject of a content report.</summary>
type RequireQualifiedAccessAttribute = inherit Attribute new: unit -> RequireQualifiedAccessAttribute
--------------------
new: unit -> RequireQualifiedAccessAttribute
<summary> User preferences that influence moderation decisions. </summary>
module Map from Microsoft.FSharp.Collections
--------------------
type Map<'Key,'Value (requires comparison)> = interface IReadOnlyDictionary<'Key,'Value> interface IReadOnlyCollection<KeyValuePair<'Key,'Value>> interface IEnumerable interface IStructuralEquatable interface IComparable interface IEnumerable<KeyValuePair<'Key,'Value>> interface ICollection<KeyValuePair<'Key,'Value>> interface IDictionary<'Key,'Value> new: elements: ('Key * 'Value) seq -> Map<'Key,'Value> member Add: key: 'Key * value: 'Value -> Map<'Key,'Value> ...
--------------------
new: elements: ('Key * 'Value) seq -> Map<'Key,'Value>
<summary> User's preferred visibility for a label. </summary>
<summary> Show a warning overlay </summary>
<summary> A label applied to content or an account. </summary>
<summary> Extract the string representation of a DID. </summary>
<param name="did">The DID to extract the value from.</param>
<returns>The full DID string (e.g. <c>"did:plc:z72i7hdynmk6r22z27h6tvur"</c>).</returns>
module Uri from FSharp.ATProto.Syntax
<summary> Functions for creating, validating, and extracting data from <see cref="Uri" /> values. </summary>
--------------------
type Uri = private | Uri of string override ToString: unit -> string
<summary> A general URI as defined by RFC 3986, used in the AT Protocol for links and references. Must have a valid scheme (starting with a letter, followed by letters, digits, <c>+</c>, <c>-</c>, or <c>.</c>) followed by <c>:</c> and a non-empty scheme-specific part with no whitespace. Maximum length is 8192 characters. </summary>
<remarks> This performs basic syntactic validation only. Valid scheme examples include <c>https</c>, <c>dns</c>, <c>at</c>, <c>did</c>, and <c>content-type</c>. See https://www.rfc-editor.org/rfc/rfc3986 for the full URI specification. </remarks>
<summary> Extract the string representation of an AT-URI. </summary>
<param name="atUri">The AT-URI to extract the value from.</param>
<returns>The full AT-URI string (e.g. <c>"at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3k2la3b"</c>).</returns>
<summary> Simplified moderation for a post with text. </summary>
<summary> Context in which moderation decisions are evaluated. Different contexts produce different actions for the same moderation cause. </summary>
<summary> Content shown in a list/feed view </summary>
<summary> The primary (highest-priority) action to take. </summary>
<summary> The action a UI should take based on moderation. </summary>
<summary> Remove from view entirely (used in list contexts) </summary>
<summary> Show with a blur/overlay that can be clicked through (unless noOverride) </summary>
<summary> Show an alert badge/indicator </summary>
<summary> Show an informational indicator </summary>
<summary> No moderation action needed </summary>