atproto/packages/api/docs/moderation.md
Matthieu Sieben b934b396b1
Client SDK rework (#2483)
* feat(api): support creation of oauth based AtpAgents

* oauth: misc fixes for confidential clients

* fix(xprc): remove ReadableStream.from polyfill

* OAuth docs tweaks (#2679)

* OAuth: clarification about client_name being shown

* OAuth: re-write handle resolution privacy concern

* avoid relying on ReadableStream.from in xrpc-server tests

* feat(oauth-types): expose "ALLOW_UNSECURE_ORIGINS" constant

* feat(handle-resolver): expose "AtprotoIdentityDidMethods" type

* fix(oauth-client): ensure that the oauth metadata document contains client_id_metadata_document_supported

* fix(oauth-types): prevent unknown query string in loopback client id

* fix(identity-resolver): check that handle is in did doc's "alsoKnownAs"

* feat(oauth-client:oauth-resolver): allow logging in using either the PDS URL or Entryway URL

* fix(oauth-client): return better error in case of invalid "oauth-protected-resource" status code

* refactor(did): group atproto specific checks in own

* feat(api): relax typing of "appLabelers" and "labelers" AtpClient properties

* allow any did as labeller (for tests mainly)

* fix(api): allow to override "atproto-proxy" on a per-request basis

* remove release candidate versions from changelog

* update changeset for api and xrpc packages

* Add missing changeset

* revert RC versions

* Proper wording in OAUTH.md api example

* remove "pre" changeset file

* xrpc: restore original behavior of setHEader and unsetHeader

* docs: add comment for XrpcClient 's constructor arg

* feat(api): expose "schemas" publicly

* feat(api): allow customizing the whatwg fetch function of the AtpAgent

* docs(api): improve migration docs

* docs: change reference to BskyAgent to AtpAgent

* docs: mention the breaking change regarding setSessionPersistHandler

* fix(api): better split AtpClient concerns

* fix(xrpc): remove unused import

* refactor(api): simplify class hierarchu by removeing AtpClient

* fix(api): mock proper method for facets detection

* restore ability to restore session asynchronously

* feat(api): allow instantiating Agent with same argument as super class

* docs(api): properly extend Agent class

* style(xrpc): var name

* docs(api): remove "async" to header getter

---------

Co-authored-by: Devin Ivy <devinivy@gmail.com>
Co-authored-by: bnewbold <bnewbold@robocracy.org>
Co-authored-by: Hailey <me@haileyok.com>
2024-08-12 19:57:21 +02:00

261 lines
7.1 KiB
Markdown

# Moderation API
Applying the moderation system is a challenging task, but we've done our best to simplify it for you. The Moderation API helps handle a wide range of tasks, including:
- Moderator labeling
- User muting (including mutelists)
- User blocking
- Mutewords
- Hidden posts
## Configuration
Every moderation function takes a set of options which look like this:
```typescript
{
// the logged-in user's DID
userDid: 'did:plc:1234...',
moderationPrefs: {
// is adult content allowed?
adultContentEnabled: true,
// the global label settings (used on self-labels)
labels: {
porn: 'hide',
sexual: 'warn',
nudity: 'ignore',
// ...
},
// the subscribed labelers and their label settings
labelers: [
{
did: 'did:plc:1234...',
labels: {
porn: 'hide',
sexual: 'warn',
nudity: 'ignore',
// ...
}
}
],
mutedWords: [/* ... */],
hiddenPosts: [/* ... */]
},
// custom label definitions
labelDefs: {
// labelerDid => defs[]
'did:plc:1234...': [
/* ... */
]
}
}
```
This should match the following interfaces:
```typescript
export interface ModerationPrefsLabeler {
did: string
labels: Record<string, LabelPreference>
}
export interface ModerationPrefs {
adultContentEnabled: boolean
labels: Record<string, LabelPreference>
labelers: ModerationPrefsLabeler[]
mutedWords: AppBskyActorDefs.MutedWord[]
hiddenPosts: string[]
}
export interface ModerationOpts {
userDid: string | undefined
prefs: ModerationPrefs
/**
* Map of labeler did -> custom definitions
*/
labelDefs?: Record<string, InterpretedLabelValueDefinition[]>
}
```
You can quickly grab the `ModerationPrefs` using the `agent.getPreferences()` method:
```typescript
const prefs = await agent.getPreferences()
moderatePost(post, {
userDid: /*...*/,
prefs: prefs.moderationPrefs,
labelDefs: /*...*/
})
```
To gather the label definitions (`labelDefs`) see the _Labelers_ section below.
## Labelers
Labelers are services that provide moderation labels. Your application will typically have 1+ top-level labelers set with the ability to do "takedowns" on content. This is controlled via this static function, though the default is to use Bluesky's moderation:
```typescript
BskyAgent.configure({
appLabelers: ['did:web:my-labeler.com'],
})
```
Users may also add their own labelers. The active labelers are controlled via an HTTP header which is automatically set by the agent when `getPreferences` is called, or when the labeler preferences are changed.
Labelers publish a `app.bsky.labeler.service` record that looks like this:
```js
{
$type: 'app.bsky.labeler.service',
policies: {
// the list of label values the labeler will publish
labelValues: [
'rude',
],
// any custom definitions the labeler will be using
labelValueDefinitions: [
{
identifier: 'rude',
blurs: 'content',
severity: 'alert',
defaultSetting: 'warn',
adultOnly: false,
locales: [
{
lang: 'en',
name: 'Rude',
description: 'Not keeping things civil.',
},
],
},
],
},
createdAt: '2024-03-12T17:17:17.215Z'
}
```
The label value definition are custom labels which only apply to that labeler. Your client needs to sync those definitions in order to correctly interpret them. To do that, call `app.bsky.labeler.getService()` (or the `getServices` batch variant) periodically to fetch their definitions. We recommend caching the response (at time our writing the official client uses a TTL of 6 hours).
Here is how to do this:
```typescript
import { AtpAgent } from '@atproto/api'
const agent = new AtpAgent({ service: 'https://example.com' })
// assume `agent` is a signed in session
const prefs = await agent.getPreferences()
const labelDefs = await agent.getLabelDefinitions(prefs)
moderatePost(post, {
userDid: agent.session.did,
prefs: prefs.moderationPrefs,
labelDefs,
})
```
## The `moderate*()` APIs
The SDK exports methods to moderate the different kinds of content on the network.
```typescript
import {
moderateProfile,
moderatePost,
moderateNotification,
moderateFeedGen,
moderateUserList,
moderateLabeler,
} from '@atproto/api'
```
Each of these follows the same API signature:
```typescript
const res = moderatePost(post, moderationOptions)
```
The response object provides an API for figuring out what your UI should do in different contexts.
```typescript
res.ui(context) /* =>
ModerationUI {
filter: boolean // should the content be removed from the interface?
blur: boolean // should the content be put behind a cover?
alert: boolean // should an alert be put on the content? (negative)
inform: boolean // should an informational notice be put on the content? (neutral)
noOverride: boolean // if blur=true, should the UI disable opening the cover?
// the reasons for each of the flags:
filters: ModerationCause[]
blurs: ModerationCause[]
alerts: ModerationCause[]
informs: ModerationCause[]
}
*/
```
There are multiple UI contexts available:
- `profileList` A profile being listed, eg in search or a follower list
- `profileView` A profile being viewed directly
- `avatar` The user's avatar in any context
- `banner` The user's banner in any context
- `displayName` The user's display name in any context
- `contentList` Content being listed, eg posts in a feed, posts as replies, a user list list, a feed generator list, etc
- `contentView` Content being viewed direct, eg an opened post, the user list page, the feedgen page, etc
- `contentMedia ` Media inside the content, eg a picture embedded in a post
Here's how a post in a feed would use these tools to make a decision:
```typescript
const mod = moderatePost(post, moderationOptions)
if (mod.ui('contentList').filter) {
// dont show the post
}
if (mod.ui('contentList').blur) {
// cover the post with the explanation from mod.ui('contentList').blurs[0]
if (mod.ui('contentList').noOverride) {
// dont allow the cover to be removed
}
}
if (mod.ui('contentMedia').blur) {
// cover the post's embedded images with the explanation from mod.ui('contentMedia').blurs[0]
if (mod.ui('contentMedia').noOverride) {
// dont allow the cover to be removed
}
}
if (mod.ui('avatar').blur) {
// cover the avatar with the explanation from mod.ui('avatar').blurs[0]
if (mod.ui('avatar').noOverride) {
// dont allow the cover to be removed
}
}
for (const alert of mod.ui('contentList').alerts) {
// render this alert
}
for (const inform of mod.ui('contentList').informs) {
// render this inform
}
```
## Sending moderation reports
Any Labeler is capable of receiving moderation reports. As a result, you need to specify which labeler should receive the report. You do this with the `Atproto-Proxy` header:
```typescript
agent
.withProxy('atproto_labeler', 'did:web:my-labeler.com')
.createModerationReport({
reasonType: 'com.atproto.moderation.defs#reasonViolation',
reason: 'They were being such a jerk to me!',
subject: { did: 'did:web:bob.com' },
})
```