Skip to content

Labels are metadata tags that get applied to records for moderation or categorization. They follow the AT Protocol labeling spec (a standard way for services to annotate content with things like "explicit" or "nsfw"). Hatk evaluates label rules automatically each time a record is indexed.

Defining a label

Create a file in server/ that exports defineLabel() with a definition and an evaluate function:

typescript
// server/labels/explicit.ts
import { defineLabel } from '$hatk'

const EXPLICIT_PATTERNS = [/\(explicit\)/i, /\[explicit\]/i, /\bexplicit version\b/i]

export default defineLabel({
  definition: {
    identifier: 'explicit',
    severity: 'inform',
    blurs: 'none',
    defaultSetting: 'warn',
    locales: [
      { lang: 'en', name: 'Explicit', description: 'Track contains explicit content' },
    ],
  },

  async evaluate(ctx) {
    if (ctx.record.collection !== 'fm.teal.alpha.feed.play') return []

    const trackName = ctx.record.value.trackName || ''
    const isExplicit = EXPLICIT_PATTERNS.some((p) => p.test(trackName))

    return isExplicit ? ['explicit'] : []
  },
})

The evaluate function runs for every indexed record. Return an array of label identifier strings to apply, or [] to skip. Labels are stored in the _labels table automatically.

Evaluate context

The evaluate function receives a context with:

FieldDescription
ctx.db.query(sql, params?)Run a SQL query against SQLite
ctx.db.run(sql, params?)Execute a SQL statement
ctx.record.uriAT URI of the record
ctx.record.cidContent hash of the record
ctx.record.didDID (decentralized identifier) of the author
ctx.record.collectionCollection NSID (e.g. fm.teal.alpha.feed.play)
ctx.record.valueThe record's fields as an object

You can query the database in evaluate for more complex rules:

typescript
async evaluate(ctx) {
  if (ctx.record.collection !== 'fm.teal.alpha.feed.play') return []

  const rows = await ctx.db.query(
    `SELECT 1 FROM explicit_tracks WHERE isrc = ? LIMIT 1`,
    [ctx.record.value.isrc],
  )

  return rows.length > 0 ? ['explicit'] : []
},

Label definition fields

FieldTypeDescription
identifierstringUnique label ID
severity'alert' | 'inform' | 'none'How urgently to surface the label
blurs'media' | 'content' | 'none'What to blur when label is applied
defaultSetting'warn' | 'hide' | 'ignore'Default user-facing behavior
localesarrayLocalized name and description

Hydrating labels in responses

Labels stored in _labels can be included in feed and query responses. The ctx.labels() helper queries active labels for a set of record URIs:

typescript
async hydrate(ctx) {
  const uris = ctx.items.map((item) => item.uri)
  const labelMap = await ctx.labels(uris)

  return ctx.items.map((item) => ({
    ...item,
    labels: labelMap.get(item.uri) || [],
  }))
},

ctx.labels() returns a Map<string, Label[]>. Expired and negated labels are automatically filtered out.