Integration guides

Go

A moderation package for Go with net/http and the standard library only.

This guide uses only the standard library.

Set the key

Put the key in the environment of your server, never in the code:

export OPENMOD_API_KEY="om_live_..."

The package

moderation/moderation.go
package moderation

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "net/url"
    "os"
    "time"
)

const base = "https://api.openshield.dev"

// Reason is a category that reached its threshold.
type Reason struct {
    ID        string  `json:"id"`
    Text      string  `json:"text"`
    Message   string  `json:"message"`
    Score     float64 `json:"score"`
    Threshold float64 `json:"threshold"`
    Source    string  `json:"source"`
}

// Decision is what a check answers.
type Decision struct {
    Action        string // "accept", "review" or "reject"
    RejectReasons []Reason
    ReviewReasons []Reason
    RequestID     string
}

// Error is an error answer of the API.
type Error struct {
    Status     int
    Type       string
    Message    string
    RetryAfter string
}

func (e *Error) Error() string {
    return fmt.Sprintf("openshield: %s (%d): %s", e.Type, e.Status, e.Message)
}

var client = &http.Client{Timeout: 10 * time.Second}

func send(ctx context.Context, path, contentType string, body io.Reader) (*Decision, error) {
    request, err := http.NewRequestWithContext(ctx, http.MethodPost, base+path, body)
    if err != nil {
        return nil, err
    }
    request.Header.Set("Authorization", "Bearer "+os.Getenv("OPENMOD_API_KEY"))
    if contentType != "" {
        request.Header.Set("Content-Type", contentType)
    }

    response, err := client.Do(request)
    if err != nil {
        return nil, err
    }
    defer response.Body.Close()
    raw, err := io.ReadAll(response.Body)
    if err != nil {
        return nil, err
    }

    if response.StatusCode != http.StatusOK {
        var failure struct {
            Error struct{ Type, Message string } `json:"error"`
        }
        _ = json.Unmarshal(raw, &failure)
        return nil, &Error{Status: response.StatusCode, Type: failure.Error.Type, Message: failure.Error.Message, RetryAfter: response.Header.Get("Retry-After")}
    }

    var answer struct {
        Summary struct {
            Action       string   `json:"action"`
            RejectReason []Reason `json:"reject_reason"`
            ReviewReason []Reason `json:"review_reason"`
        } `json:"summary"`
        RequestID string `json:"request_id"`
    }
    if err := json.Unmarshal(raw, &answer); err != nil {
        return nil, err
    }
    return &Decision{
        Action:        answer.Summary.Action,
        RejectReasons: answer.Summary.RejectReason,
        ReviewReasons: answer.Summary.ReviewReason,
        RequestID:     answer.RequestID,
    }, nil
}

// CheckText checks a text. ref is your own id for it, or empty.
func CheckText(ctx context.Context, text, ref string) (*Decision, error) {
    payload := map[string]any{"text": text}
    if ref != "" {
        payload["ref"] = ref
    }
    encoded, err := json.Marshal(payload)
    if err != nil {
        return nil, err
    }
    return send(ctx, "/v1/text", "application/json", bytes.NewReader(encoded))
}

// CheckImage checks an image sent as bytes. ref is your own id for it, or empty.
func CheckImage(ctx context.Context, image []byte, ref string) (*Decision, error) {
    path := "/v1/image"
    if ref != "" {
        path += "?ref=" + url.QueryEscape(ref)
    }
    return send(ctx, path, "", bytes.NewReader(image))
}

Use it, and fail closed

func moderateComment(ctx context.Context, text, commentID string) string {
    decision, err := moderation.CheckText(ctx, text, commentID)
    if err != nil {
        var failure *moderation.Error
        // The content cannot be accepted as it is: refuse it.
        if errors.As(err, &failure) && (failure.Status == 400 || failure.Status == 413 || failure.Status == 415) {
            return "refuse"
        }
        // Anything else, a timeout included: keep it hidden until someone looks at it.
        return "hold"
    }
    switch decision.Action {
    case "accept":
        return "publish"
    case "review":
        return "hold"
    default:
        return "refuse"
    }
}

For rejected content, show your user a message chosen from the ID of the first reject reason, in your own words and your own languages. Best practices says more.