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.