package tools import ( "bytes" "compress/gzip" "encoding/json" "fmt" "io" "net/http" "runtime/debug" "strings" ) type APIError struct { Status int `json:"-"` Code int `json:"code"` Message string `json:"message"` } var ( ERROR_GENERIC_SERVER = APIError{Status: 500, Code: 10000, Message: "Server Error"} ERROR_GENERIC_NOT_FOUND = APIError{Status: 404, Code: 10001, Message: "Endpoint Not Found"} ERROR_GENERIC_RATELIMIT = APIError{Status: 429, Code: 10002, Message: "Too Many Requests"} ERROR_GENERIC_UNAUTHORIZED = APIError{Status: 401, Code: 10003, Message: "Unauthorized"} ERROR_GENERIC_FORBIDDEN = APIError{Status: 403, Code: 10004, Message: "Forbidden"} ERROR_GENERIC_METHOD_NOT_ALLOWED = APIError{Status: 405, Code: 10005, Message: "Method Not Allowed"} ERROR_SERVER_RESOURCES_EXHAUSTED = APIError{Status: 507, Code: 11006, Message: "Resources Exhausted"} ERROR_BODY_EMPTY = APIError{Status: 411, Code: 12000, Message: "Request Body is Empty"} ERROR_BODY_TOO_LARGE = APIError{Status: 413, Code: 12001, Message: "Request Body is Too Large"} ERROR_BODY_INVALID_CONTENT_TYPE = APIError{Status: 400, Code: 12002, Message: "Invalid 'Content-Type' Header"} ERROR_BODY_INVALID_CHALLENGE = APIError{Status: 400, Code: 12003, Message: "Invalid 'X-Challenge-*' Header"} ERROR_BODY_INVALID_FIELD = APIError{Status: 400, Code: 12004, Message: "Invalid Body Field"} ERROR_BODY_INVALID_DATA = APIError{Status: 422, Code: 12005, Message: "Invalid Body"} ERROR_UNKNOWN_ENDPOINT = APIError{Status: 404, Code: 13000, Message: "Unknown Endpoint"} ERROR_UNKNOWN_FUNCTION = APIError{Status: 404, Code: 13001, Message: "Unknown Function"} ERROR_UNKNOWN_ANIMATION = APIError{Status: 404, Code: 13002, Message: "Unknown Animation"} ERROR_UNKNOWN_TASK = APIError{Status: 404, Code: 13003, Message: "Unknown Task"} ERROR_UNKNOWN_CHALLENGE = APIError{Status: 404, Code: 13004, Message: "Unknown Challenge"} ERROR_CHALLENGE_INVALID = APIError{Status: 400, Code: 14000, Message: "Invalid Challenge Result"} ERROR_CHALLENGE_TOO_EASY = APIError{Status: 400, Code: 14001, Message: "Challenge Difficulty Too Low"} ERROR_CHALLENGE_EXPIRED = APIError{Status: 400, Code: 14002, Message: "Challenge Expired"} ERROR_MEDIA_INVALID = APIError{Status: 400, Code: 15000, Message: "Media Invalid"} ERROR_MEDIA_INAPPROPRIATE = APIError{Status: 400, Code: 15001, Message: "Media Inappropriate"} ) // Reject request due to a Client Mistake func SendClientError(w http.ResponseWriter, r *http.Request, err APIError) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(err.Status) fmt.Fprintf(w, `{"code":%d,"message":%q}`, err.Code, err.Message) } // Reject request due to a Server Error // Additionally collects debug information and logs it to the console func SendServerError(w http.ResponseWriter, r *http.Request, err error) { debugStack := strings.Split(string(debug.Stack()), "\n") for i, item := range debugStack { debugStack[i] = strings.ReplaceAll(item, "\t", " ") } if len(debugStack) > 5 { debugStack = debugStack[5:] // skip header } reqHeader := make(map[string]string, len(r.Header)) for key, header := range r.Header { reqHeader[key] = strings.Join(header, ", ") } LoggerHTTP.Data(ERROR, err.Error(), map[string]any{ "request": map[string]any{ "method": r.Method, "url": r.URL.String(), "headers": reqHeader, }, "error": map[string]any{ "raw": err, "message": err.Error(), "stack": debugStack, }, }) if w != nil { SendClientError(w, r, ERROR_GENERIC_SERVER) } } // Respond to the request with a JSON object func SendJSON(w http.ResponseWriter, r *http.Request, statusCode int, responseObject any) (int, error) { // Check Compression var g io.Writer if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { w.Header().Set("Content-Encoding", "gzip") z := gzip.NewWriter(w) defer z.Close() g = z } else { g = w } // Stream Object w.Header().Set("Content-Type", "application/json") w.WriteHeader(statusCode) if b, ok := responseObject.([]byte); ok { return g.Write(b) } else { j := json.NewEncoder(g) return 0, j.Encode(responseObject) } } // Encode Object as JSON and gzipped version func PrepareStaticJSON(responseObject any) ([]byte, []byte, error) { // Encode Object buf, err := json.Marshal(responseObject) if err != nil { return nil, nil, err } // Compress Object cmp := bytes.Buffer{} zip := gzip.NewWriter(&cmp) if _, err := zip.Write(buf); err != nil { zip.Close() return nil, nil, err } if err := zip.Close(); err != nil { return nil, nil, err } return buf, cmp.Bytes(), nil } // Respond to the request with a Static JSON Object func SendStaticJSON(w http.ResponseWriter, r *http.Request, statusCode int, content []byte, gzipped []byte) (int, error) { w.Header().Set("Content-Type", "application/json") if gzipped != nil && strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { w.Header().Set("Content-Encoding", "gzip") return w.Write(gzipped) } return w.Write(content) }