Skip to content

Reblog Controls API

Manage export filters and blocklists for the Curated Queue.

Overview

The Reblog Controls API provides fine-grained control over which posts are eligible for export via the Curated Queue. You can block specific users, hashtags, and configure export settings (media requirements, etc.). These filters are applied after the manual review/approval workflow, ensuring only posts matching your curation criteria reach external consumers.

Filter Architecture

Approved Posts → Export Filters → Curated Queue → External Tools (e.g., Zhongli)
                  ↓
              Blocked Users
              Blocked Hashtags
              Media Requirements
              Other Settings

Authentication

All Reblog Controls API endpoints require API key authentication via X-API-Key header:

curl -H "X-API-Key: your-api-key" \
  http://localhost:8000/api/v1/reblog-controls/settings

See Authentication Guide for details.

Endpoints

GET /api/v1/reblog-controls/settings

Get current export filter settings.

Request

curl -H "X-API-Key: your-api-key" \
  http://localhost:8000/api/v1/reblog-controls/settings

Response (200 OK)

{
  "require_media": false,
  "auto_reject_blocked_users": true,
  "auto_reject_blocked_hashtags": true,
  "created_at": "2026-02-15T10:00:00Z",
  "updated_at": "2026-03-03T14:30:00Z"
}

Response Fields

Field Type Description
require_media boolean Only export posts with media attachments
auto_reject_blocked_users boolean Auto-reject posts from blocked users during fetch
auto_reject_blocked_hashtags boolean Auto-reject posts with blocked hashtags during fetch
created_at string ISO 8601 creation timestamp
updated_at string ISO 8601 last update timestamp

PUT /api/v1/reblog-controls/settings

Update export filter settings.

Request

curl -X PUT \
  -H "X-API-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "require_media": true,
    "auto_reject_blocked_users": true,
    "auto_reject_blocked_hashtags": true
  }' \
  http://localhost:8000/api/v1/reblog-controls/settings

Request Body

{
  "require_media": true,
  "auto_reject_blocked_users": true,
  "auto_reject_blocked_hashtags": true
}
Field Type Required Description
require_media boolean No Require media attachments for export
auto_reject_blocked_users boolean No Auto-reject on fetch
auto_reject_blocked_hashtags boolean No Auto-reject on fetch

Response (200 OK)

{
  "require_media": true,
  "auto_reject_blocked_users": true,
  "auto_reject_blocked_hashtags": true,
  "updated_at": "2026-03-03T15:00:00Z"
}

GET /api/v1/reblog-controls/blocked-users

List all blocked users.

Request

curl -H "X-API-Key: your-api-key" \
  "http://localhost:8000/api/v1/reblog-controls/blocked-users?skip=0&limit=50"

Query Parameters

Parameter Type Default Description
skip integer 0 Number of items to skip
limit integer 100 Number of items to return

Response (200 OK)

{
  "items": [
    {
      "id": 1,
      "account_id": "https://example.com/users/spammer",
      "username": "spammer",
      "instance": "example.com",
      "reason": "Persistent spam promoter",
      "created_at": "2026-02-20T12:00:00Z"
    },
    {
      "id": 2,
      "account_id": "https://badactors.com/users/troll",
      "username": "troll",
      "instance": "badactors.com",
      "reason": "Harassment and misinformation",
      "created_at": "2026-02-25T09:30:00Z"
    }
  ],
  "total": 2,
  "skip": 0,
  "limit": 50
}

Response Fields

Field Type Description
id integer Blocklist entry identifier
account_id string ActivityPub user URI
username string Fediverse username
instance string Instance domain
reason string Why user is blocked
created_at string ISO 8601 block time

POST /api/v1/reblog-controls/blocked-users

Add a user to the blocklist.

Request

curl -X POST \
  -H "X-API-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "https://example.com/users/spammer",
    "reason": "Persistent spam promoter"
  }' \
  http://localhost:8000/api/v1/reblog-controls/blocked-users

Request Body

{
  "account_id": "https://example.com/users/spammer",
  "reason": "Persistent spam promoter"
}
Field Type Required Description
account_id string Yes Full ActivityPub user URI or @user@instance format
reason string No Blocking reason (for records)

Response (201 Created)

{
  "id": 3,
  "account_id": "https://example.com/users/spammer",
  "username": "spammer",
  "instance": "example.com",
  "reason": "Persistent spam promoter",
  "created_at": "2026-03-03T15:30:00Z"
}

Error Responses

{ "detail": "User already blocked" }  // 409
{ "detail": "Invalid account format" }  // 400

DELETE /api/v1/reblog-controls/blocked-users/{id}

Remove a user from the blocklist.

Request

curl -X DELETE \
  -H "X-API-Key: your-api-key" \
  http://localhost:8000/api/v1/reblog-controls/blocked-users/1

Response (204 No Content)

Error Responses

{ "detail": "Blocked user not found" }  // 404

GET /api/v1/reblog-controls/blocked-hashtags

List all blocked hashtags.

Request

curl -H "X-API-Key: your-api-key" \
  "http://localhost:8000/api/v1/reblog-controls/blocked-hashtags?skip=0&limit=50"

Query Parameters

Parameter Type Default Description
skip integer 0 Number of items to skip
limit integer 100 Number of items to return

Response (200 OK)

{
  "items": [
    {
      "id": 1,
      "hashtag": "cryptocurrency",
      "reason": "Off-topic spam promotion",
      "created_at": "2026-02-18T14:00:00Z"
    },
    {
      "id": 2,
      "hashtag": "buymecoffe",
      "reason": "Commercial spam",
      "created_at": "2026-02-20T10:15:00Z"
    }
  ],
  "total": 2,
  "skip": 0,
  "limit": 50
}

Response Fields

Field Type Description
id integer Blocklist entry identifier
hashtag string Hashtag name (without #)
reason string Why hashtag is blocked
created_at string ISO 8601 block time

POST /api/v1/reblog-controls/blocked-hashtags

Add a hashtag to the blocklist.

Request

curl -X POST \
  -H "X-API-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "hashtag": "cryptocurrency",
    "reason": "Off-topic spam promotion"
  }' \
  http://localhost:8000/api/v1/reblog-controls/blocked-hashtags

Request Body

{
  "hashtag": "cryptocurrency",
  "reason": "Off-topic spam promotion"
}
Field Type Required Description
hashtag string Yes Hashtag to block (without #)
reason string No Blocking reason

Response (201 Created)

{
  "id": 3,
  "hashtag": "cryptocurrency",
  "reason": "Off-topic spam promotion",
  "created_at": "2026-03-03T15:45:00Z"
}

Error Responses

{ "detail": "Hashtag already blocked" }  // 409

DELETE /api/v1/reblog-controls/blocked-hashtags/{id}

Remove a hashtag from the blocklist.

Request

curl -X DELETE \
  -H "X-API-Key: your-api-key" \
  http://localhost:8000/api/v1/reblog-controls/blocked-hashtags/1

Response (204 No Content)

Error Responses

{ "detail": "Blocked hashtag not found" }  // 404

Examples

Python

import httpx

api_key = "your-api-key"
base_url = "http://localhost:8000/api/v1/reblog-controls"
headers = {"X-API-Key": api_key}

# Get current settings
async with httpx.AsyncClient() as client:
    response = await client.get(f"{base_url}/settings", headers=headers)
    settings = response.json()
    print(f"Require media: {settings['require_media']}")

# Block a user
async with httpx.AsyncClient() as client:
    response = await client.post(
        f"{base_url}/blocked-users",
        json={
            "account_id": "@spammer@example.com",
            "reason": "Persistent spam"
        },
        headers=headers
    )
    blocked = response.json()
    print(f"Blocked user {blocked['username']}")

# List blocked users
async with httpx.AsyncClient() as client:
    response = await client.get(
        f"{base_url}/blocked-users",
        headers=headers
    )
    users = response.json()["items"]
    print(f"Total blocked users: {len(users)}")

# Block a hashtag
async with httpx.AsyncClient() as client:
    response = await client.post(
        f"{base_url}/blocked-hashtags",
        json={
            "hashtag": "nsfw",
            "reason": "Not suitable for feed"
        },
        headers=headers
    )
    blocked = response.json()
    print(f"Blocked hashtag: #{blocked['hashtag']}")

# Update settings
async with httpx.AsyncClient() as client:
    response = await client.put(
        f"{base_url}/settings",
        json={"require_media": True},
        headers=headers
    )
    updated = response.json()
    print(f"Updated settings")

JavaScript

const apiKey = "your-api-key";
const baseUrl = "http://localhost:8000/api/v1/reblog-controls";
const headers = { "X-API-Key": apiKey };

// Get settings
const settings = await fetch(`${baseUrl}/settings`, { headers })
  .then(r => r.json());

// Block a user
const blockedUser = await fetch(
  `${baseUrl}/blocked-users`,
  {
    method: "POST",
    headers: { ...headers, "Content-Type": "application/json" },
    body: JSON.stringify({
      account_id: "@spammer@example.com",
      reason: "Persistent spam"
    })
  }
).then(r => r.json());

// List blocked users
const blockedUsers = await fetch(
  `${baseUrl}/blocked-users`,
  { headers }
).then(r => r.json());

// Block a hashtag
const blockedHashtag = await fetch(
  `${baseUrl}/blocked-hashtags`,
  {
    method: "POST",
    headers: { ...headers, "Content-Type": "application/json" },
    body: JSON.stringify({
      hashtag: "nsfw",
      reason: "Not suitable"
    })
  }
).then(r => r.json());

// Update settings
const updated = await fetch(
  `${baseUrl}/settings`,
  {
    method: "PUT",
    headers: { ...headers, "Content-Type": "application/json" },
    body: JSON.stringify({ require_media: true })
  }
).then(r => r.json());

cURL

export API_KEY="your-api-key"
export BASE_URL="http://localhost:8000/api/v1/reblog-controls"

# Get settings
curl -H "X-API-Key: $API_KEY" "$BASE_URL/settings"

# Block a user
curl -X POST "$BASE_URL/blocked-users" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "@spammer@example.com",
    "reason": "Persistent spam"
  }'

# List blocked users
curl -H "X-API-Key: $API_KEY" "$BASE_URL/blocked-users"

# Remove blocked user
curl -X DELETE "$BASE_URL/blocked-users/1" \
  -H "X-API-Key: $API_KEY"

# Block a hashtag
curl -X POST "$BASE_URL/blocked-hashtags" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"hashtag":"nsfw","reason":"Not suitable"}'

# List blocked hashtags
curl -H "X-API-Key: $API_KEY" "$BASE_URL/blocked-hashtags"

# Remove blocked hashtag
curl -X DELETE "$BASE_URL/blocked-hashtags/1" \
  -H "X-API-Key: $API_KEY"

# Update settings
curl -X PUT "$BASE_URL/settings" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"require_media":true}'

Filter Strategies

Strategy 1: Permissive (Default)

  • Block only known bad actors and obvious spam hashtags
  • Allow most approved posts through
  • Good for discovering new quality content
# Configuration
require_media: false
auto_reject_blocked_users: true
auto_reject_blocked_hashtags: true
blocked_users: [known spammers]
blocked_hashtags: [obvious spam hashtags]

Strategy 2: Restrictive

  • Require media for all exports
  • Block more users/hashtags
  • Good for high-quality curation
# Configuration
require_media: true
auto_reject_blocked_users: true
auto_reject_blocked_hashtags: true
blocked_users: [many accounts]
blocked_hashtags: [broad categories]

Strategy 3: Custom (Per-Stream)

Use separate FenLiu instances with different settings for different purposes: - Instance A: High-volume discovery (permissive) - Instance B: Quality flagship content (restrictive)


Common Patterns

Block a Category of Users

# Block all cryptocurrency scam accounts
for account in @cryptoscam1@spam.com @cryptoscam2@badactors.com; do
  curl -X POST "$BASE_URL/blocked-users" \
    -H "X-API-Key: $API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"account_id\": \"$account\", \"reason\": \"Cryptocurrency scam\"}"
done

Media-Only Export

# Only export posts with images/videos
curl -X PUT "$BASE_URL/settings" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"require_media": true}'

Temporary Block

# Block a hashtag temporarily
curl -X POST "$BASE_URL/blocked-hashtags" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"hashtag": "temporary", "reason": "Blocked during event"}'

# Later, unblock when done
curl -X DELETE "$BASE_URL/blocked-hashtags/123" \
  -H "X-API-Key: $API_KEY"

Auto-Reject During Fetch

When auto_reject_blocked_users or auto_reject_blocked_hashtags is enabled:

  1. Posts are fetched from Fediverse
  2. Posts from blocked users → rejected automatically
  3. Posts with blocked hashtags → rejected automatically
  4. Remaining posts → added to database for review

This saves review effort by filtering known-bad content at fetch time.

Manual Review vs. Filters

  • Manual Review: Humans approve/reject individual posts (flexible)
  • Auto Filters: Block entire users/hashtags (efficient for spam)
  • Combined: Use both for defense-in-depth approach


Best Practices

  1. Start Permissive: Begin with minimal blocks, add as needed
  2. Document Reasons: Record why users/hashtags are blocked
  3. Regular Review: Periodically review blocklist for false positives
  4. Gradual Escalation: Consider suspending users before blocking
  5. Community Input: Consider feedback from reviewers when blocking
  6. Monitor Impact: Track how filters affect post volume and quality