feat: initial release with 48 Mattermost MCP tools
- 27 base tools: channels, messaging, users, reactions, files, DMs - Team management: invite/remove users, get stats, list members - Slash commands: execute /remind, /poll, etc. - Webhook management: incoming and outgoing webhooks - System tools: server config, logs, bulk status updates - Channel admin: create, invite, leave, delete channels - Read-only mode for safe exploration - Dual token support (bot + PAT) for enhanced security - Apache 2.0 licensed
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ErrorCategory string
|
||||
|
||||
const (
|
||||
CategoryFile ErrorCategory = "file"
|
||||
CategoryAuth ErrorCategory = "auth"
|
||||
CategoryRepo ErrorCategory = "repo"
|
||||
CategoryChannel ErrorCategory = "channel"
|
||||
CategoryUser ErrorCategory = "user"
|
||||
CategoryMessage ErrorCategory = "message"
|
||||
CategoryNetwork ErrorCategory = "network"
|
||||
CategoryUnknown ErrorCategory = "unknown"
|
||||
)
|
||||
|
||||
type EnhancedError struct {
|
||||
Original error
|
||||
Translated string
|
||||
Category ErrorCategory
|
||||
Operation string
|
||||
Context map[string]string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
func (e *EnhancedError) Error() string {
|
||||
if e.Translated != "" {
|
||||
return e.Translated
|
||||
}
|
||||
if e.Original != nil {
|
||||
return e.Original.Error()
|
||||
}
|
||||
return "unknown error"
|
||||
}
|
||||
|
||||
func (e *EnhancedError) Unwrap() error {
|
||||
return e.Original
|
||||
}
|
||||
|
||||
func (e *EnhancedError) WithContext(key, value string) *EnhancedError {
|
||||
if e.Context == nil {
|
||||
e.Context = make(map[string]string)
|
||||
}
|
||||
e.Context[key] = value
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *EnhancedError) WithOperation(op string) *EnhancedError {
|
||||
e.Operation = op
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *EnhancedError) WithParam(key, value string) *EnhancedError {
|
||||
return e.WithContext(key, value)
|
||||
}
|
||||
|
||||
func (e *EnhancedError) FormatDetailed() string {
|
||||
details := map[string]any{
|
||||
"error": e.Error(),
|
||||
"category": e.Category,
|
||||
"timestamp": e.Timestamp.Format(time.RFC3339),
|
||||
}
|
||||
|
||||
if e.Operation != "" {
|
||||
details["operation"] = e.Operation
|
||||
}
|
||||
|
||||
if len(e.Context) > 0 {
|
||||
details["context"] = e.Context
|
||||
}
|
||||
|
||||
if e.Original != nil && e.Original.Error() != e.Error() {
|
||||
details["original"] = e.Original.Error()
|
||||
}
|
||||
|
||||
jsonBytes, err := json.MarshalIndent(details, "", " ")
|
||||
if err != nil {
|
||||
return e.Format()
|
||||
}
|
||||
|
||||
return string(jsonBytes)
|
||||
}
|
||||
|
||||
func (e *EnhancedError) Format() string {
|
||||
var parts []string
|
||||
|
||||
if e.Operation != "" {
|
||||
parts = append(parts, fmt.Sprintf("Operation: %s", e.Operation))
|
||||
}
|
||||
|
||||
parts = append(parts, fmt.Sprintf("Error: %s", e.Error()))
|
||||
|
||||
if e.Category != "" && e.Category != CategoryUnknown {
|
||||
parts = append(parts, fmt.Sprintf("Category: %s", e.Category))
|
||||
}
|
||||
|
||||
if len(e.Context) > 0 {
|
||||
var ctxParts []string
|
||||
for k, v := range e.Context {
|
||||
ctxParts = append(ctxParts, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("Context: %s", strings.Join(ctxParts, ", ")))
|
||||
}
|
||||
|
||||
if e.Original != nil && e.Original.Error() != e.Error() {
|
||||
parts = append(parts, fmt.Sprintf("Original: %s", e.Original.Error()))
|
||||
}
|
||||
|
||||
return strings.Join(parts, " | ")
|
||||
}
|
||||
|
||||
func TranslateError(err error, context map[string]string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var existing *EnhancedError
|
||||
if errors.As(err, &existing) {
|
||||
if context != nil {
|
||||
for k, v := range context {
|
||||
existing.WithContext(k, v)
|
||||
}
|
||||
}
|
||||
return existing
|
||||
}
|
||||
|
||||
translated, category := translateErrorMessage(err)
|
||||
|
||||
operation := ""
|
||||
if context != nil {
|
||||
operation = context["operation"]
|
||||
}
|
||||
|
||||
enhanced := &EnhancedError{
|
||||
Original: err,
|
||||
Translated: translated,
|
||||
Category: category,
|
||||
Operation: operation,
|
||||
Context: context,
|
||||
Timestamp: time.Now().UTC(),
|
||||
}
|
||||
|
||||
return enhanced
|
||||
}
|
||||
|
||||
func translateErrorMessage(err error) (string, ErrorCategory) {
|
||||
if err == nil {
|
||||
return "", CategoryUnknown
|
||||
}
|
||||
|
||||
msg := err.Error()
|
||||
lowerMsg := strings.ToLower(msg)
|
||||
|
||||
if strings.Contains(msg, "404") {
|
||||
return "Resource not found", CategoryUnknown
|
||||
}
|
||||
|
||||
if strings.Contains(msg, "401") {
|
||||
return "Authentication failed - check your access token", CategoryAuth
|
||||
}
|
||||
|
||||
if strings.Contains(msg, "403") {
|
||||
return "Permission denied - you don't have access to this resource", CategoryAuth
|
||||
}
|
||||
|
||||
translations := []struct {
|
||||
pattern string
|
||||
message string
|
||||
category ErrorCategory
|
||||
}{
|
||||
{"GetUser", "User not found", CategoryUser},
|
||||
{"GetChannel", "Channel not found", CategoryChannel},
|
||||
{"GetPost", "Message not found", CategoryMessage},
|
||||
{"CreatePost", "Failed to create message", CategoryMessage},
|
||||
{"UpdatePost", "Failed to update message", CategoryMessage},
|
||||
{"DeletePost", "Failed to delete message", CategoryMessage},
|
||||
{"CreateChannel", "Failed to create channel", CategoryChannel},
|
||||
}
|
||||
|
||||
for _, t := range translations {
|
||||
if strings.Contains(msg, t.pattern) {
|
||||
return t.message, t.category
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(lowerMsg, "timeout") || strings.Contains(lowerMsg, "deadline exceeded") {
|
||||
return "Request timed out - the server took too long to respond", CategoryNetwork
|
||||
}
|
||||
|
||||
if strings.Contains(lowerMsg, "connection refused") || strings.Contains(lowerMsg, "no such host") {
|
||||
return "Network error - cannot connect to server", CategoryNetwork
|
||||
}
|
||||
|
||||
return msg, CategoryUnknown
|
||||
}
|
||||
|
||||
func IsNotFound(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var enhanced *EnhancedError
|
||||
if errors.As(err, &enhanced) {
|
||||
switch enhanced.Category {
|
||||
case CategoryFile, CategoryRepo, CategoryChannel, CategoryUser, CategoryMessage:
|
||||
return true
|
||||
}
|
||||
return strings.Contains(enhanced.Translated, "not found")
|
||||
}
|
||||
|
||||
var httpErr interface{ Error() string }
|
||||
if errors.As(err, &httpErr) {
|
||||
if strings.Contains(httpErr.Error(), "404") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
lowerMsg := strings.ToLower(err.Error())
|
||||
return strings.Contains(lowerMsg, "not found") || strings.Contains(lowerMsg, "404")
|
||||
}
|
||||
|
||||
func IsAuthError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var enhanced *EnhancedError
|
||||
if errors.As(err, &enhanced) {
|
||||
return enhanced.Category == CategoryAuth
|
||||
}
|
||||
|
||||
msg := err.Error()
|
||||
if strings.Contains(msg, "401") || strings.Contains(msg, "403") {
|
||||
return true
|
||||
}
|
||||
|
||||
lowerMsg := strings.ToLower(msg)
|
||||
return strings.Contains(lowerMsg, "authentication") ||
|
||||
strings.Contains(lowerMsg, "unauthorized") ||
|
||||
strings.Contains(lowerMsg, "permission denied") ||
|
||||
strings.Contains(lowerMsg, "forbidden")
|
||||
}
|
||||
|
||||
func IsTimeout(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var enhanced *EnhancedError
|
||||
if errors.As(err, &enhanced) {
|
||||
return enhanced.Category == CategoryNetwork || strings.Contains(enhanced.Translated, "timed out")
|
||||
}
|
||||
|
||||
lowerMsg := strings.ToLower(err.Error())
|
||||
return strings.Contains(lowerMsg, "timeout") ||
|
||||
strings.Contains(lowerMsg, "deadline exceeded") ||
|
||||
strings.Contains(lowerMsg, "context deadline")
|
||||
}
|
||||
|
||||
func IsNetworkError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var enhanced *EnhancedError
|
||||
if errors.As(err, &enhanced) {
|
||||
return enhanced.Category == CategoryNetwork
|
||||
}
|
||||
|
||||
lowerMsg := strings.ToLower(err.Error())
|
||||
return strings.Contains(lowerMsg, "connection") ||
|
||||
strings.Contains(lowerMsg, "network") ||
|
||||
strings.Contains(lowerMsg, "no such host") ||
|
||||
strings.Contains(lowerMsg, "dial tcp")
|
||||
}
|
||||
|
||||
func NewEnhancedError(original error, translated string, category ErrorCategory) *EnhancedError {
|
||||
return &EnhancedError{
|
||||
Original: original,
|
||||
Translated: translated,
|
||||
Category: category,
|
||||
Context: make(map[string]string),
|
||||
Timestamp: time.Now().UTC(),
|
||||
}
|
||||
}
|
||||
|
||||
func Wrap(err error, operation string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return TranslateError(err, map[string]string{"operation": operation})
|
||||
}
|
||||
|
||||
type HTTPError interface {
|
||||
error
|
||||
Status() int
|
||||
}
|
||||
|
||||
type statusError struct {
|
||||
status int
|
||||
message string
|
||||
}
|
||||
|
||||
func (e *statusError) Error() string { return e.message }
|
||||
func (e *statusError) Status() int { return e.status }
|
||||
|
||||
func IsHTTPError(err error, statusCode int) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var httpErr HTTPError
|
||||
if errors.As(err, &httpErr) {
|
||||
return httpErr.Status() == statusCode
|
||||
}
|
||||
|
||||
msg := err.Error()
|
||||
return strings.Contains(msg, fmt.Sprintf("status %d", statusCode)) ||
|
||||
strings.Contains(msg, fmt.Sprintf("%d", statusCode))
|
||||
}
|
||||
|
||||
func IsUnauthorized(err error) bool {
|
||||
return IsHTTPError(err, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func IsForbidden(err error) bool {
|
||||
return IsHTTPError(err, http.StatusForbidden)
|
||||
}
|
||||
|
||||
func IsNotFoundHTTP(err error) bool {
|
||||
return IsHTTPError(err, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func IsServerError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var httpErr HTTPError
|
||||
if errors.As(err, &httpErr) {
|
||||
return httpErr.Status() >= 500 && httpErr.Status() < 600
|
||||
}
|
||||
|
||||
msg := err.Error()
|
||||
for i := 500; i < 600; i++ {
|
||||
if strings.Contains(msg, fmt.Sprintf("status %d", i)) ||
|
||||
strings.Contains(msg, fmt.Sprintf("%d", i)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user