Header menu logo FSharp.ATProto

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

ReasonSpam

Unsolicited or repetitive content

ReasonViolation

Terms of service or community guideline violation

ReasonMisleading

Deceptive or misleading content

ReasonSexual

Unwanted sexual content

ReasonRude

Rude or disrespectful behavior

ReasonOther

Does not fit other categories (provide a description)

ReasonAppeal

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:

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

ModerationPrefs

User's label visibility settings, muted words, hidden posts, adult content preference

ModerationDecision

Computed decision with prioritized causes

ModerationAction

What to do: Blur, Alert, Filter, Inform, or NoOp

ModerationContext

Where the content appears: ProfileList, ProfileView, Avatar, Banner, ContentList, ContentView, ContentMedia

Label

A label applied to content: value, source DID, negation flag

LabelDefinition

Built-in label behavior definition

CustomLabelValueDef

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

Moderation.moderatePost

Compute decision for a post (labels + muted words + hidden posts)

Moderation.moderateProfile

Compute decision for a profile

Moderation.moderateNotification

Compute decision for a notification

Moderation.moderateFeedGenerator

Compute decision for a feed generator

Moderation.moderateUserList

Compute decision for a user list

Moderation.moderate

General-purpose moderation (labels + mute/block state + target type)

Labels.findLabel

Look up a built-in label definition

Labels.interpretLabelValueDefinition

Convert a labeler's custom label to a CustomLabelValueDef

Multiple items
namespace FSharp

--------------------
namespace Microsoft.FSharp
namespace FSharp.ATProto
namespace FSharp.ATProto.Syntax
namespace FSharp.ATProto.Core
namespace FSharp.ATProto.Bluesky
val agent: AtpAgent
module Unchecked from Microsoft.FSharp.Core.Operators
val defaultof<'T> : 'T
Multiple items
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>
val post: TimelinePost
Multiple items
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>
val postRef: PostRef
type PostRef = { Uri: AtUri Cid: Cid }
<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>
val someHandle: Handle
Multiple items
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>
val listUri: AtUri
Multiple items
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://&lt;authority&gt;[/&lt;collection&gt;[/&lt;rkey&gt;]]</c>. Maximum length is 8192 characters. </summary>
<remarks> See the AT Protocol specification: https://atproto.com/specs/at-uri-scheme </remarks>
val userDid: Did
Multiple items
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>
val postUri: AtUri
val labelerDid: Did
val taskResult: TaskResultBuilder
val profile: Profile
module Bluesky from FSharp.ATProto.Bluesky
<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>
val getProfile: agent: AtpAgent -> actor: 'a -> System.Threading.Tasks.Task<Result<Profile,XrpcError>> (requires member ToActorString)
<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>
val muteUser: agent: AtpAgent -> target: 'a -> System.Threading.Tasks.Task<Result<unit,XrpcError>> (requires member ToDid)
<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>
val unmuteUser: agent: AtpAgent -> target: 'a -> System.Threading.Tasks.Task<Result<unit,XrpcError>> (requires member ToDid)
<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>
val muteUserByHandle: agent: AtpAgent -> identifier: string -> System.Threading.Tasks.Task<Result<unit,XrpcError>>
<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>
val muteThread: agent: AtpAgent -> root: 'a -> System.Threading.Tasks.Task<Result<unit,XrpcError>> (requires member ToAtUri)
<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>
val unmuteThread: agent: AtpAgent -> root: 'a -> System.Threading.Tasks.Task<Result<unit,XrpcError>> (requires member ToAtUri)
<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>
val muteModList: agent: AtpAgent -> listUri: AtUri -> System.Threading.Tasks.Task<Result<unit,XrpcError>>
<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>
val unmuteModList: agent: AtpAgent -> listUri: AtUri -> System.Threading.Tasks.Task<Result<unit,XrpcError>>
<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>
val blockRef: ListBlockRef
val blockModList: agent: AtpAgent -> listUri: AtUri -> System.Threading.Tasks.Task<Result<ListBlockRef,XrpcError>>
<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>
val unblockModList: agent: AtpAgent -> listBlockRef: ListBlockRef -> System.Threading.Tasks.Task<Result<unit,XrpcError>>
<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>
val reportId: int64
val reportContent: agent: AtpAgent -> subject: ReportSubject -> reason: ComAtprotoModeration.Defs.ReasonType -> description: string option -> System.Threading.Tasks.Task<Result<int64,XrpcError>>
<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>
type ReportSubject = | Account of Did | Record of PostRef
<summary>The subject of a content report.</summary>
union case ReportSubject.Record: PostRef -> ReportSubject
module ComAtprotoModeration from FSharp.ATProto.Bluesky
module Defs from FSharp.ATProto.Bluesky.ComAtprotoModeration
type ReasonType = | ReasonSpam | ReasonViolation | ReasonMisleading | ReasonSexual | ReasonRude | ReasonOther | ReasonAppeal | ReasonAppeal2 | ReasonOther2 | ReasonViolenceAnimal ...
union case ComAtprotoModeration.Defs.ReasonType.ReasonSpam: ComAtprotoModeration.Defs.ReasonType
union case Option.Some: Value: 'T -> Option<'T>
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
Multiple items
type RequireQualifiedAccessAttribute = inherit Attribute new: unit -> RequireQualifiedAccessAttribute

--------------------
new: unit -> RequireQualifiedAccessAttribute
type ReportSubject = | Account of obj | Record of obj
union case ReportSubject.Account: Did -> ReportSubject
union case ComAtprotoModeration.Defs.ReasonType.ReasonViolation: ComAtprotoModeration.Defs.ReasonType
union case Option.None: Option<'T>
Profile.IsMuted: bool
Profile.IsBlocking: bool
Profile.IsBlockedBy: bool
val output: ComAtprotoModeration.CreateReport.Output
module CreateReport from FSharp.ATProto.Bluesky.ComAtprotoModeration
val call: agent: AtpAgent -> input: ComAtprotoModeration.CreateReport.Input -> System.Threading.Tasks.Task<Result<ComAtprotoModeration.CreateReport.Output,XrpcError>>
type InputSubjectUnion = | RepoRef of RepoRef | StrongRef of StrongRef | Unknown of string * JsonElement
union case ComAtprotoModeration.CreateReport.InputSubjectUnion.RepoRef: ComAtprotoAdmin.Defs.RepoRef -> ComAtprotoModeration.CreateReport.InputSubjectUnion
ComAtprotoModeration.CreateReport.Output.Id: int64
ComAtprotoModeration.CreateReport.Output.CreatedAt: AtDateTime
namespace FSharp.ATProto.Moderation
val prefs: ModerationPrefs
type ModerationPrefs = { AdultContentEnabled: bool Labels: Map<string,LabelVisibility> MutedWords: MutedWord list HiddenPosts: string list } static member Default: ModerationPrefs with get
<summary> User preferences that influence moderation decisions. </summary>
module Labels from FSharp.ATProto.Moderation
Multiple items
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>
val ofList: elements: ('Key * 'T) list -> Map<'Key,'T> (requires comparison)
type LabelVisibility = | Show | Warn | Hide
<summary> User's preferred visibility for a label. </summary>
union case LabelVisibility.Warn: LabelVisibility
<summary> Show a warning overlay </summary>
module MutedWords from FSharp.ATProto.Moderation
val labels: Label list
type Label = { Src: string Uri: string Val: string Neg: bool Cts: string option }
<summary> A label applied to content or an account. </summary>
type 'T list = List<'T>
val value: Did -> string
<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>
Multiple items
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>
val value: AtUri -> string
<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>
val decision: ModerationDecision
module Moderation from FSharp.ATProto.Moderation
val moderatePost: prefs: ModerationPrefs -> labels: Label list -> text: string -> tags: string list -> languages: string list -> isHiddenPost: bool -> isMe: bool -> userDid: string option -> context: ModerationContext -> ModerationDecision
<summary> Simplified moderation for a post with text. </summary>
type ModerationContext = | ContentList | ContentView | ProfileList | ProfileView | Avatar | Banner | DisplayName | ContentMedia
<summary> Context in which moderation decisions are evaluated. Different contexts produce different actions for the same moderation cause. </summary>
union case ModerationContext.ContentList: ModerationContext
<summary> Content shown in a list/feed view </summary>
property ModerationDecision.Action: ModerationAction with get
<summary> The primary (highest-priority) action to take. </summary>
type ModerationAction = | Filter | Blur | Alert | Inform | NoAction
<summary> The action a UI should take based on moderation. </summary>
union case ModerationAction.Filter: ModerationAction
<summary> Remove from view entirely (used in list contexts) </summary>
union case ModerationAction.Blur: ModerationAction
<summary> Show with a blur/overlay that can be clicked through (unless noOverride) </summary>
union case ModerationAction.Alert: ModerationAction
<summary> Show an alert badge/indicator </summary>
union case ModerationAction.Inform: ModerationAction
<summary> Show an informational indicator </summary>
union case ModerationAction.NoAction: ModerationAction
<summary> No moderation action needed </summary>

Type something to start searching.