35a0b2b715
- 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
890 lines
27 KiB
Go
890 lines
27 KiB
Go
// Package mattermost provides a wrapper around the Mattermost Go client
|
|
// with dual token support, timeout handling, retry logic, and error categorization.
|
|
package mattermost
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/karti-ai/mattermost-mcp-server/pkg/errors"
|
|
"github.com/karti-ai/mattermost-mcp-server/pkg/log"
|
|
"github.com/mattermost/mattermost-server/v6/model"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
const (
|
|
// DefaultTimeout is the default timeout for all API requests
|
|
DefaultTimeout = 30 * time.Second
|
|
// MaxRetries is the maximum number of retries for 5xx errors
|
|
MaxRetries = 3
|
|
// InitialBackoff is the initial backoff duration for retries
|
|
InitialBackoff = 500 * time.Millisecond
|
|
)
|
|
|
|
// Client wraps the Mattermost Go client with enhanced functionality
|
|
type Client struct {
|
|
client *model.Client4
|
|
botToken string
|
|
pat string
|
|
host string
|
|
}
|
|
|
|
// Global client instance for use by operation handlers
|
|
var globalClient *Client
|
|
|
|
// SetGlobalClient sets the global client instance
|
|
func SetGlobalClient(c *Client) {
|
|
globalClient = c
|
|
}
|
|
|
|
// GetGlobalClient returns the global client instance
|
|
func GetGlobalClient() *Client {
|
|
return globalClient
|
|
}
|
|
|
|
// NewClient creates a new Mattermost client with the given configuration
|
|
func NewClient(host, botToken, pat string) *Client {
|
|
c := model.NewAPIv4Client(host)
|
|
// Initialize HTTPHeader map to store custom headers
|
|
c.HTTPHeader = make(map[string]string)
|
|
return &Client{
|
|
client: c,
|
|
botToken: botToken,
|
|
pat: pat,
|
|
host: host,
|
|
}
|
|
}
|
|
|
|
// getToken returns the appropriate token based on operation type
|
|
// Uses BotToken for read operations (GET), PAT for write operations (POST/PUT/DELETE)
|
|
func (c *Client) getToken(isWrite bool) string {
|
|
if isWrite && c.pat != "" {
|
|
return c.pat
|
|
}
|
|
if c.botToken != "" {
|
|
return c.botToken
|
|
}
|
|
return c.pat
|
|
}
|
|
|
|
// setToken sets the appropriate token on the client based on HTTP method
|
|
func (c *Client) setToken(method string) {
|
|
isWrite := method != http.MethodGet && method != http.MethodHead
|
|
token := c.getToken(isWrite)
|
|
if token != "" {
|
|
c.client.HTTPHeader["Authorization"] = "Bearer " + token
|
|
}
|
|
}
|
|
|
|
// isRetryableError checks if an error warrants a retry
|
|
func isRetryableError(resp *model.Response, err error) bool {
|
|
if resp == nil {
|
|
return true // Network errors should be retried
|
|
}
|
|
return resp.StatusCode >= 500 && resp.StatusCode < 600
|
|
}
|
|
|
|
// calculateBackoff calculates the backoff duration for retry attempts
|
|
func calculateBackoff(attempt int) time.Duration {
|
|
// Exponential backoff: 500ms, 1s, 2s
|
|
backoff := InitialBackoff * time.Duration(math.Pow(2, float64(attempt)))
|
|
// Add jitter
|
|
return backoff + time.Duration(time.Now().UnixNano()%100)*time.Millisecond
|
|
}
|
|
|
|
// logRequest logs API request details (NEVER logs tokens)
|
|
func (c *Client) logRequest(ctx context.Context, method, path string) {
|
|
logger := log.WithContext(ctx)
|
|
logger.Debug("sending API request",
|
|
zap.String("method", method),
|
|
zap.String("path", path),
|
|
zap.String("host", c.host),
|
|
)
|
|
}
|
|
|
|
// logResponse logs API response details
|
|
func (c *Client) logResponse(ctx context.Context, method, path string, statusCode int, duration time.Duration, err error) {
|
|
logger := log.WithContext(ctx)
|
|
if err != nil {
|
|
logger.Error("API request failed",
|
|
zap.String("method", method),
|
|
zap.String("path", path),
|
|
zap.Int("status_code", statusCode),
|
|
zap.Duration("duration", duration),
|
|
zap.Error(err),
|
|
)
|
|
} else {
|
|
logger.Debug("API request completed",
|
|
zap.String("method", method),
|
|
zap.String("path", path),
|
|
zap.Int("status_code", statusCode),
|
|
zap.Duration("duration", duration),
|
|
)
|
|
}
|
|
}
|
|
|
|
// mapError converts Mattermost errors to categorized errors
|
|
func (c *Client) mapError(resp *model.Response, err error, operation string) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
var category errors.ErrorCategory
|
|
var translated string
|
|
statusCode := 0
|
|
|
|
if resp != nil {
|
|
statusCode = resp.StatusCode
|
|
}
|
|
|
|
switch statusCode {
|
|
case 401:
|
|
category = errors.CategoryAuth
|
|
translated = "Authentication failed - check your access token"
|
|
case 403:
|
|
category = errors.CategoryAuth
|
|
translated = "Permission denied - you don't have access to this resource"
|
|
case 404:
|
|
category = errors.CategoryUnknown
|
|
translated = "Resource not found"
|
|
case 429:
|
|
category = errors.CategoryNetwork
|
|
translated = "Rate limited - too many requests, please try again later"
|
|
case 500, 502, 503, 504:
|
|
category = errors.CategoryNetwork
|
|
translated = "Server error - the Mattermost server encountered a problem"
|
|
default:
|
|
if resp == nil {
|
|
category = errors.CategoryNetwork
|
|
translated = "Connection failed - unable to reach Mattermost server"
|
|
} else {
|
|
// Try to extract error message from Mattermost API error
|
|
if appErr, ok := err.(*model.AppError); ok && appErr.Message != "" {
|
|
translated = appErr.Message
|
|
category = c.categorizeAppError(appErr)
|
|
} else {
|
|
translated = err.Error()
|
|
category = errors.CategoryUnknown
|
|
}
|
|
}
|
|
}
|
|
|
|
enhanced := errors.NewEnhancedError(err, translated, category)
|
|
enhanced.WithOperation(operation)
|
|
|
|
return enhanced
|
|
}
|
|
|
|
// categorizeAppError determines error category from Mattermost AppError
|
|
func (c *Client) categorizeAppError(appErr *model.AppError) errors.ErrorCategory {
|
|
id := strings.ToLower(appErr.Id)
|
|
message := strings.ToLower(appErr.Message)
|
|
|
|
// Check error ID patterns
|
|
switch {
|
|
case strings.Contains(id, "authentication") || strings.Contains(id, "auth"):
|
|
return errors.CategoryAuth
|
|
case strings.Contains(id, "channel"):
|
|
return errors.CategoryChannel
|
|
case strings.Contains(id, "user"):
|
|
return errors.CategoryUser
|
|
case strings.Contains(id, "post") || strings.Contains(id, "message"):
|
|
return errors.CategoryMessage
|
|
case strings.Contains(id, "file"):
|
|
return errors.CategoryFile
|
|
}
|
|
|
|
// Check message patterns
|
|
switch {
|
|
case strings.Contains(message, "channel") && strings.Contains(message, "not found"):
|
|
return errors.CategoryChannel
|
|
case strings.Contains(message, "user") && strings.Contains(message, "not found"):
|
|
return errors.CategoryUser
|
|
case strings.Contains(message, "post") && strings.Contains(message, "not found"):
|
|
return errors.CategoryMessage
|
|
case strings.Contains(message, "file") && strings.Contains(message, "not found"):
|
|
return errors.CategoryFile
|
|
case strings.Contains(message, "permission") || strings.Contains(message, "unauthorized"):
|
|
return errors.CategoryAuth
|
|
}
|
|
|
|
return errors.CategoryUnknown
|
|
}
|
|
|
|
// executeWithRetry executes an API call with retry logic for 5xx errors
|
|
func (c *Client) executeWithRetry(
|
|
ctx context.Context,
|
|
operation string,
|
|
method string,
|
|
path string,
|
|
fn func() (*model.Response, error),
|
|
) (*model.Response, error) {
|
|
ctx, cancel := context.WithTimeout(ctx, DefaultTimeout)
|
|
defer cancel()
|
|
|
|
c.setToken(method)
|
|
c.logRequest(ctx, method, path)
|
|
|
|
start := time.Now()
|
|
var lastErr error
|
|
var resp *model.Response
|
|
|
|
for attempt := 0; attempt <= MaxRetries; attempt++ {
|
|
if attempt > 0 {
|
|
backoff := calculateBackoff(attempt - 1)
|
|
log.WithContext(ctx).Debug("retrying request",
|
|
zap.String("operation", operation),
|
|
zap.Int("attempt", attempt),
|
|
zap.Duration("backoff", backoff),
|
|
)
|
|
time.Sleep(backoff)
|
|
}
|
|
|
|
resp, lastErr = fn()
|
|
duration := time.Since(start)
|
|
|
|
if lastErr == nil {
|
|
if resp != nil {
|
|
c.logResponse(ctx, method, path, resp.StatusCode, duration, nil)
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
if !isRetryableError(resp, lastErr) {
|
|
c.logResponse(ctx, method, path, 0, duration, lastErr)
|
|
return resp, c.mapError(resp, lastErr, operation)
|
|
}
|
|
|
|
// Log retryable error
|
|
if resp != nil {
|
|
log.WithContext(ctx).Warn("retryable error occurred",
|
|
zap.String("operation", operation),
|
|
zap.Int("attempt", attempt),
|
|
zap.Int("status_code", resp.StatusCode),
|
|
zap.Error(lastErr),
|
|
)
|
|
}
|
|
}
|
|
|
|
// All retries exhausted
|
|
duration := time.Since(start)
|
|
c.logResponse(ctx, method, path, 0, duration, fmt.Errorf("max retries exceeded: %w", lastErr))
|
|
return resp, c.mapError(resp, fmt.Errorf("max retries exceeded: %w", lastErr), operation)
|
|
}
|
|
|
|
// ==================== User Operations ====================
|
|
|
|
// GetMe retrieves the current authenticated user
|
|
func (c *Client) GetMe(ctx context.Context) (*model.User, error) {
|
|
c.setToken(http.MethodGet)
|
|
user, resp, err := c.client.GetMe("")
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "GetMe")
|
|
}
|
|
return user, nil
|
|
}
|
|
|
|
// GetUser retrieves a user by ID
|
|
func (c *Client) GetUser(ctx context.Context, userID string) (*model.User, error) {
|
|
c.setToken(http.MethodGet)
|
|
user, resp, err := c.client.GetUser(userID, "")
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "GetUser")
|
|
}
|
|
return user, nil
|
|
}
|
|
|
|
// GetUserByUsername retrieves a user by username
|
|
func (c *Client) GetUserByUsername(ctx context.Context, username string) (*model.User, error) {
|
|
c.setToken(http.MethodGet)
|
|
user, resp, err := c.client.GetUserByUsername(username, "")
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "GetUserByUsername")
|
|
}
|
|
return user, nil
|
|
}
|
|
|
|
// SearchUsers searches for users based on search criteria
|
|
func (c *Client) SearchUsers(ctx context.Context, search *model.UserSearch) ([]*model.User, error) {
|
|
c.setToken(http.MethodPost)
|
|
users, resp, err := c.client.SearchUsers(search)
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "SearchUsers")
|
|
}
|
|
return users, nil
|
|
}
|
|
|
|
// GetUserStatus retrieves the status of a user (online, away, offline, dnd)
|
|
func (c *Client) GetUserStatus(ctx context.Context, userID string) (*model.Status, error) {
|
|
c.setToken(http.MethodGet)
|
|
status, resp, err := c.client.GetUserStatus(userID, "")
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "GetUserStatus")
|
|
}
|
|
return status, nil
|
|
}
|
|
|
|
// ==================== Channel Operations ====================
|
|
|
|
// GetChannel retrieves a channel by ID
|
|
func (c *Client) GetChannel(ctx context.Context, channelID string) (*model.Channel, error) {
|
|
c.setToken(http.MethodGet)
|
|
channel, resp, err := c.client.GetChannel(channelID, "")
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "GetChannel")
|
|
}
|
|
return channel, nil
|
|
}
|
|
|
|
// GetChannelByName retrieves a channel by name in a team
|
|
func (c *Client) GetChannelByName(ctx context.Context, teamID, channelName string) (*model.Channel, error) {
|
|
c.setToken(http.MethodGet)
|
|
channel, resp, err := c.client.GetChannelByName(channelName, teamID, "")
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "GetChannelByName")
|
|
}
|
|
return channel, nil
|
|
}
|
|
|
|
// GetChannelsForTeamForUser retrieves channels for a user in a team
|
|
func (c *Client) GetChannelsForTeamForUser(ctx context.Context, teamID, userID string, includeDeleted bool) ([]*model.Channel, error) {
|
|
c.setToken(http.MethodGet)
|
|
channels, resp, err := c.client.GetChannelsForTeamForUser(teamID, userID, includeDeleted, "")
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "GetChannelsForTeamForUser")
|
|
}
|
|
return channels, nil
|
|
}
|
|
|
|
// CreateDirectChannel creates a direct message channel between two users
|
|
func (c *Client) CreateDirectChannel(ctx context.Context, userID1, userID2 string) (*model.Channel, error) {
|
|
c.setToken(http.MethodPost)
|
|
channel, resp, err := c.client.CreateDirectChannel(userID1, userID2)
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "CreateDirectChannel")
|
|
}
|
|
return channel, nil
|
|
}
|
|
|
|
// GetTeamUnread retrieves unread counts for a team
|
|
func (c *Client) GetTeamUnread(ctx context.Context, userID, teamID string) (*model.TeamUnread, error) {
|
|
c.setToken(http.MethodGet)
|
|
unread, resp, err := c.client.GetTeamUnread(userID, teamID)
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "GetTeamUnread")
|
|
}
|
|
return unread, nil
|
|
}
|
|
|
|
// GetChannelMembersForUser retrieves channel members for a user in a team
|
|
func (c *Client) GetChannelMembersForUser(ctx context.Context, userID, teamID string) (model.ChannelMembers, error) {
|
|
c.setToken(http.MethodGet)
|
|
members, resp, err := c.client.GetChannelMembersForUser(userID, teamID, "")
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "GetChannelMembersForUser")
|
|
}
|
|
return members, nil
|
|
}
|
|
|
|
// MarkChannelAsRead marks a channel as read for a user
|
|
func (c *Client) MarkChannelAsRead(ctx context.Context, channelID, userID string) (*model.ChannelViewResponse, error) {
|
|
c.setToken(http.MethodPut)
|
|
viewResp, resp, err := c.client.ViewChannel(userID, &model.ChannelView{ChannelId: channelID, PrevChannelId: ""})
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "MarkChannelAsRead")
|
|
}
|
|
return viewResp, nil
|
|
}
|
|
|
|
// ==================== Post Operations ====================
|
|
|
|
// GetPostsForChannel retrieves posts for a channel
|
|
func (c *Client) GetPostsForChannel(ctx context.Context, channelID string, page, perPage int, collapsedThreads bool) (*model.PostList, error) {
|
|
c.setToken(http.MethodGet)
|
|
posts, resp, err := c.client.GetPostsForChannel(channelID, page, perPage, "", collapsedThreads)
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "GetPostsForChannel")
|
|
}
|
|
return posts, nil
|
|
}
|
|
|
|
// SearchPosts searches for posts using terms
|
|
func (c *Client) SearchPosts(ctx context.Context, teamID, terms string, isOrSearch bool) (*model.PostList, error) {
|
|
c.setToken(http.MethodPost)
|
|
results, resp, err := c.client.SearchPosts(teamID, terms, isOrSearch)
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "SearchPosts")
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// CreatePost creates a new post in a channel
|
|
func (c *Client) CreatePost(ctx context.Context, post *model.Post) (*model.Post, error) {
|
|
c.setToken(http.MethodPost)
|
|
created, resp, err := c.client.CreatePost(post)
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "CreatePost")
|
|
}
|
|
return created, nil
|
|
}
|
|
|
|
// UpdatePost updates an existing post
|
|
func (c *Client) UpdatePost(ctx context.Context, postID string, post *model.Post) (*model.Post, error) {
|
|
c.setToken(http.MethodPut)
|
|
updated, resp, err := c.client.UpdatePost(postID, post)
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "UpdatePost")
|
|
}
|
|
return updated, nil
|
|
}
|
|
|
|
// DeletePost deletes a post by ID
|
|
func (c *Client) DeletePost(ctx context.Context, postID string) error {
|
|
_, err := c.executeWithRetry(
|
|
ctx,
|
|
"DeletePost",
|
|
http.MethodDelete,
|
|
fmt.Sprintf("/api/v4/posts/%s", postID),
|
|
func() (*model.Response, error) {
|
|
r, e := c.client.DeletePost(postID)
|
|
return r, e
|
|
},
|
|
)
|
|
return err
|
|
}
|
|
|
|
// ==================== Reaction Operations ====================
|
|
|
|
// GetReactions retrieves all reactions for a post
|
|
func (c *Client) GetReactions(ctx context.Context, postID string) ([]*model.Reaction, error) {
|
|
c.setToken(http.MethodGet)
|
|
reactions, resp, err := c.client.GetReactions(postID)
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "GetReactions")
|
|
}
|
|
return reactions, nil
|
|
}
|
|
|
|
// SaveReaction adds a reaction to a post
|
|
func (c *Client) SaveReaction(ctx context.Context, reaction *model.Reaction) (*model.Reaction, error) {
|
|
c.setToken(http.MethodPost)
|
|
saved, resp, err := c.client.SaveReaction(reaction)
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "SaveReaction")
|
|
}
|
|
return saved, nil
|
|
}
|
|
|
|
// DeleteReaction removes a reaction from a post
|
|
func (c *Client) DeleteReaction(ctx context.Context, reaction *model.Reaction) error {
|
|
_, err := c.executeWithRetry(
|
|
ctx,
|
|
"DeleteReaction",
|
|
http.MethodDelete,
|
|
"/api/v4/reactions",
|
|
func() (*model.Response, error) {
|
|
r, e := c.client.DeleteReaction(reaction)
|
|
return r, e
|
|
},
|
|
)
|
|
return err
|
|
}
|
|
|
|
// ==================== File Operations ====================
|
|
|
|
// UploadFile uploads a file to a channel
|
|
func (c *Client) UploadFile(ctx context.Context, data []byte, channelID, filename string) (*model.FileUploadResponse, error) {
|
|
c.setToken(http.MethodPost)
|
|
uploadResp, resp, err := c.client.UploadFile(data, channelID, filename)
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "UploadFile")
|
|
}
|
|
return uploadResp, nil
|
|
}
|
|
|
|
// GetFile retrieves a file by ID
|
|
func (c *Client) GetFile(ctx context.Context, fileID string) ([]byte, *model.Response, error) {
|
|
c.setToken(http.MethodGet)
|
|
data, resp, err := c.client.GetFile(fileID)
|
|
if err != nil {
|
|
return nil, resp, c.mapError(resp, err, "GetFile")
|
|
}
|
|
return data, resp, nil
|
|
}
|
|
|
|
// ==================== Team Operations ====================
|
|
|
|
// GetTeamsForUser retrieves all teams a user is a member of
|
|
func (c *Client) GetTeamsForUser(ctx context.Context, userID string) ([]*model.Team, error) {
|
|
c.setToken(http.MethodGet)
|
|
teams, resp, err := c.client.GetTeamsForUser(userID, "")
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "GetTeamsForUser")
|
|
}
|
|
return teams, nil
|
|
}
|
|
|
|
// GetChannelPosts retrieves posts for a channel with pagination support
|
|
func (c *Client) GetChannelPosts(ctx context.Context, channelID string, limit int, before, after string) (*model.PostList, error) {
|
|
c.setToken(http.MethodGet)
|
|
|
|
var posts *model.PostList
|
|
var resp *model.Response
|
|
var err error
|
|
|
|
if before != "" {
|
|
posts, resp, err = c.client.GetPostsBefore(channelID, before, 0, limit, "", false)
|
|
} else if after != "" {
|
|
posts, resp, err = c.client.GetPostsAfter(channelID, after, 0, limit, "", false)
|
|
} else {
|
|
posts, resp, err = c.client.GetPostsForChannel(channelID, 0, limit, "", false)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "GetChannelPosts")
|
|
}
|
|
return posts, nil
|
|
}
|
|
|
|
// GetPostThread retrieves all posts in a thread (parent + replies)
|
|
func (c *Client) GetPostThread(ctx context.Context, postID string) (*model.PostList, error) {
|
|
c.setToken(http.MethodGet)
|
|
posts, resp, err := c.client.GetPostThread(postID, "", false)
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "GetPostThread")
|
|
}
|
|
return posts, nil
|
|
}
|
|
|
|
// GetChannelMembers retrieves all members of a channel
|
|
func (c *Client) GetChannelMembers(ctx context.Context, channelID string, page, perPage int) (model.ChannelMembers, error) {
|
|
c.setToken(http.MethodGet)
|
|
members, resp, err := c.client.GetChannelMembers(channelID, page, perPage, "")
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "GetChannelMembers")
|
|
}
|
|
return members, nil
|
|
}
|
|
|
|
// CreateGroupChannel creates a group DM channel with multiple users
|
|
func (c *Client) CreateGroupChannel(ctx context.Context, userIDs []string) (*model.Channel, error) {
|
|
c.setToken(http.MethodPost)
|
|
channel, resp, err := c.client.CreateGroupChannel(userIDs)
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "CreateGroupChannel")
|
|
}
|
|
return channel, nil
|
|
}
|
|
|
|
// UpdateUserStatus updates the current user's status (online, away, dnd, offline)
|
|
func (c *Client) UpdateUserStatus(ctx context.Context, userID string, status string) (*model.Status, error) {
|
|
c.setToken(http.MethodPut)
|
|
userStatus := &model.Status{
|
|
UserId: userID,
|
|
Status: status,
|
|
}
|
|
updated, resp, err := c.client.UpdateUserStatus(userID, userStatus)
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "UpdateUserStatus")
|
|
}
|
|
return updated, nil
|
|
}
|
|
|
|
// ==================== Admin Channel Operations ====================
|
|
|
|
// CreateChannel creates a new channel in a team
|
|
func (c *Client) CreateChannel(ctx context.Context, channel *model.Channel) (*model.Channel, error) {
|
|
c.setToken(http.MethodPost)
|
|
created, resp, err := c.client.CreateChannel(channel)
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "CreateChannel")
|
|
}
|
|
return created, nil
|
|
}
|
|
|
|
// AddChannelMember adds a user to a channel
|
|
func (c *Client) AddChannelMember(ctx context.Context, channelID, userID string) (*model.ChannelMember, error) {
|
|
c.setToken(http.MethodPost)
|
|
member, resp, err := c.client.AddChannelMember(channelID, userID)
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "AddChannelMember")
|
|
}
|
|
return member, nil
|
|
}
|
|
|
|
// DeleteChannel deletes/archival a channel (soft delete by default)
|
|
func (c *Client) DeleteChannel(ctx context.Context, channelID string) error {
|
|
_, err := c.executeWithRetry(
|
|
ctx,
|
|
"DeleteChannel",
|
|
http.MethodDelete,
|
|
fmt.Sprintf("/api/v4/channels/%s", channelID),
|
|
func() (*model.Response, error) {
|
|
r, e := c.client.DeleteChannel(channelID)
|
|
return r, e
|
|
},
|
|
)
|
|
return err
|
|
}
|
|
|
|
// GetChannelStats retrieves statistics for a channel including member count
|
|
func (c *Client) GetChannelStats(ctx context.Context, channelID string) (*model.ChannelStats, error) {
|
|
c.setToken(http.MethodGet)
|
|
stats, resp, err := c.client.GetChannelStats(channelID, "")
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "GetChannelStats")
|
|
}
|
|
return stats, nil
|
|
}
|
|
|
|
// RemoveChannelMember removes a user from a channel
|
|
func (c *Client) RemoveChannelMember(ctx context.Context, channelID, userID string) error {
|
|
_, err := c.executeWithRetry(
|
|
ctx,
|
|
"RemoveChannelMember",
|
|
http.MethodDelete,
|
|
fmt.Sprintf("/api/v4/channels/%s/members/%s", channelID, userID),
|
|
func() (*model.Response, error) {
|
|
r, e := c.client.RemoveUserFromChannel(channelID, userID)
|
|
return r, e
|
|
},
|
|
)
|
|
return err
|
|
}
|
|
|
|
// ==================== Pin Operations ====================
|
|
|
|
// PinPost pins a post to a channel
|
|
func (c *Client) PinPost(ctx context.Context, postID string) error {
|
|
c.setToken(http.MethodPost)
|
|
_, err := c.client.PinPost(postID)
|
|
if err != nil {
|
|
return c.mapError(nil, err, "PinPost")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UnpinPost unpins a post from a channel
|
|
func (c *Client) UnpinPost(ctx context.Context, postID string) error {
|
|
c.setToken(http.MethodPost)
|
|
_, err := c.client.UnpinPost(postID)
|
|
if err != nil {
|
|
return c.mapError(nil, err, "UnpinPost")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetPinnedPosts retrieves all pinned posts in a channel
|
|
func (c *Client) GetPinnedPosts(ctx context.Context, channelID string) (*model.PostList, error) {
|
|
c.setToken(http.MethodGet)
|
|
posts, resp, err := c.client.GetPinnedPosts(channelID, "")
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "GetPinnedPosts")
|
|
}
|
|
return posts, nil
|
|
}
|
|
|
|
// ==================== Bulk Status Operations ====================
|
|
|
|
// GetUsersStatus retrieves status for multiple users by IDs
|
|
func (c *Client) GetUsersStatus(ctx context.Context, userIDs []string) ([]*model.Status, error) {
|
|
c.setToken(http.MethodPost)
|
|
statuses, resp, err := c.client.GetUsersStatusesByIds(userIDs)
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "GetUsersStatus")
|
|
}
|
|
return statuses, nil
|
|
}
|
|
|
|
// ==================== Webhook Operations ====================
|
|
|
|
// CreateIncomingWebhook creates an incoming webhook for a channel
|
|
func (c *Client) CreateIncomingWebhook(ctx context.Context, channelID string, displayName string) (*model.IncomingWebhook, error) {
|
|
c.setToken(http.MethodPost)
|
|
|
|
// Get current user for webhook creation
|
|
me, err := c.GetMe(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get current user: %w", err)
|
|
}
|
|
|
|
hook := &model.IncomingWebhook{
|
|
ChannelId: channelID,
|
|
DisplayName: displayName,
|
|
UserId: me.Id,
|
|
}
|
|
|
|
created, resp, err := c.client.CreateIncomingWebhook(hook)
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "CreateIncomingWebhook")
|
|
}
|
|
return created, nil
|
|
}
|
|
|
|
// ListIncomingWebhooks lists incoming webhooks for a team
|
|
func (c *Client) ListIncomingWebhooks(ctx context.Context, teamID string, page, perPage int) ([]*model.IncomingWebhook, error) {
|
|
c.setToken(http.MethodGet)
|
|
hooks, resp, err := c.client.GetIncomingWebhooksForTeam(teamID, page, perPage, "")
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "ListIncomingWebhooks")
|
|
}
|
|
return hooks, nil
|
|
}
|
|
|
|
// DeleteIncomingWebhook deletes an incoming webhook
|
|
func (c *Client) DeleteIncomingWebhook(ctx context.Context, hookID string) error {
|
|
_, err := c.executeWithRetry(
|
|
ctx,
|
|
"DeleteIncomingWebhook",
|
|
http.MethodDelete,
|
|
fmt.Sprintf("/api/v4/hooks/incoming/%s", hookID),
|
|
func() (*model.Response, error) {
|
|
r, e := c.client.DeleteIncomingWebhook(hookID)
|
|
return r, e
|
|
},
|
|
)
|
|
return err
|
|
}
|
|
|
|
// ==================== Slash Commands ====================
|
|
|
|
// ExecuteSlashCommand runs a slash command in a channel
|
|
func (c *Client) ExecuteSlashCommand(ctx context.Context, channelID string, command string) (*model.CommandResponse, error) {
|
|
c.setToken(http.MethodPost)
|
|
resp, _, err := c.client.ExecuteCommand(channelID, command)
|
|
if err != nil {
|
|
return nil, c.mapError(nil, err, "ExecuteSlashCommand")
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
// ==================== Team Administration ====================
|
|
|
|
// InviteUserToTeam adds a user to a team
|
|
func (c *Client) InviteUserToTeam(ctx context.Context, teamID, userID string) (*model.TeamMember, error) {
|
|
c.setToken(http.MethodPost)
|
|
member, resp, err := c.client.AddTeamMember(teamID, userID)
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "InviteUserToTeam")
|
|
}
|
|
return member, nil
|
|
}
|
|
|
|
// RemoveUserFromTeam removes a user from a team
|
|
func (c *Client) RemoveUserFromTeam(ctx context.Context, teamID, userID string) error {
|
|
_, err := c.executeWithRetry(
|
|
ctx,
|
|
"RemoveUserFromTeam",
|
|
http.MethodDelete,
|
|
fmt.Sprintf("/api/v4/teams/%s/members/%s", teamID, userID),
|
|
func() (*model.Response, error) {
|
|
r, e := c.client.RemoveTeamMember(teamID, userID)
|
|
return r, e
|
|
},
|
|
)
|
|
return err
|
|
}
|
|
|
|
// ListTeamMembers gets members of a team
|
|
func (c *Client) ListTeamMembers(ctx context.Context, teamID string, page, perPage int) ([]*model.TeamMember, error) {
|
|
c.setToken(http.MethodGet)
|
|
members, resp, err := c.client.GetTeamMembers(teamID, page, perPage, "")
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "ListTeamMembers")
|
|
}
|
|
return members, nil
|
|
}
|
|
|
|
// GetTeamStats gets statistics for a team
|
|
func (c *Client) GetTeamStats(ctx context.Context, teamID string) (*model.TeamStats, error) {
|
|
c.setToken(http.MethodGet)
|
|
stats, resp, err := c.client.GetTeamStats(teamID, "")
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "GetTeamStats")
|
|
}
|
|
return stats, nil
|
|
}
|
|
|
|
// ==================== Post Operations ====================
|
|
|
|
// GetPost retrieves a single post by ID
|
|
func (c *Client) GetPost(ctx context.Context, postID string) (*model.Post, error) {
|
|
c.setToken(http.MethodGet)
|
|
post, resp, err := c.client.GetPost(postID, "")
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "GetPost")
|
|
}
|
|
return post, nil
|
|
}
|
|
|
|
// ==================== Outgoing Webhooks ====================
|
|
|
|
// CreateOutgoingWebhook creates an outgoing webhook
|
|
func (c *Client) CreateOutgoingWebhook(ctx context.Context, teamID string, displayName string, triggerWords []string, callbackURL string) (*model.OutgoingWebhook, error) {
|
|
c.setToken(http.MethodPost)
|
|
|
|
hook := &model.OutgoingWebhook{
|
|
TeamId: teamID,
|
|
DisplayName: displayName,
|
|
TriggerWords: triggerWords,
|
|
CallbackURLs: []string{callbackURL},
|
|
}
|
|
|
|
created, resp, err := c.client.CreateOutgoingWebhook(hook)
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "CreateOutgoingWebhook")
|
|
}
|
|
return created, nil
|
|
}
|
|
|
|
// ListOutgoingWebhooks lists outgoing webhooks for a team
|
|
func (c *Client) ListOutgoingWebhooks(ctx context.Context, teamID string, page, perPage int) ([]*model.OutgoingWebhook, error) {
|
|
c.setToken(http.MethodGet)
|
|
hooks, resp, err := c.client.GetOutgoingWebhooksForTeam(teamID, page, perPage, "")
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "ListOutgoingWebhooks")
|
|
}
|
|
return hooks, nil
|
|
}
|
|
|
|
// DeleteOutgoingWebhook deletes an outgoing webhook
|
|
func (c *Client) DeleteOutgoingWebhook(ctx context.Context, hookID string) error {
|
|
_, err := c.executeWithRetry(
|
|
ctx,
|
|
"DeleteOutgoingWebhook",
|
|
http.MethodDelete,
|
|
fmt.Sprintf("/api/v4/hooks/outgoing/%s", hookID),
|
|
func() (*model.Response, error) {
|
|
r, e := c.client.DeleteOutgoingWebhook(hookID)
|
|
return r, e
|
|
},
|
|
)
|
|
return err
|
|
}
|
|
|
|
// ==================== System & Config ====================
|
|
|
|
// GetSystemLogs retrieves system logs (requires admin)
|
|
func (c *Client) GetSystemLogs(ctx context.Context, page, perPage int) ([]string, *model.Response, error) {
|
|
c.setToken(http.MethodGet)
|
|
logs, resp, err := c.client.GetLogs(page, perPage)
|
|
if err != nil {
|
|
return nil, resp, c.mapError(resp, err, "GetSystemLogs")
|
|
}
|
|
return logs, resp, nil
|
|
}
|
|
|
|
// GetConfig retrieves server configuration (requires admin)
|
|
func (c *Client) GetConfig(ctx context.Context) (*model.Config, error) {
|
|
c.setToken(http.MethodGet)
|
|
config, resp, err := c.client.GetConfig()
|
|
if err != nil {
|
|
return nil, c.mapError(resp, err, "GetConfig")
|
|
}
|
|
return config, nil
|
|
}
|