package main import ( "fmt" "strings" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin" ) const botUsername = "mattermore" const botDisplayName = "Mattermore AI" const botDescription = "AI chat agent powered by Ollama." // OnActivate is invoked when the plugin is activated. func (p *Plugin) OnActivate() error { // Register the /ai slash command. if err := p.API.RegisterCommand(&model.Command{ Trigger: "ai", DisplayName: "Mattermore", Description: "AI chat agent powered by Ollama.", AutoComplete: true, AutoCompleteDesc: "Chat with the AI. Usage: /ai ", AutoCompleteHint: "", }); err != nil { return fmt.Errorf("register command: %w", err) } // Ensure the bot account exists. bot := &model.Bot{ Username: botUsername, DisplayName: botDisplayName, Description: botDescription, } createdBot, appErr := p.API.CreateBot(bot) if appErr != nil { return fmt.Errorf("create bot: %w", appErr) } p.botUserID = createdBot.UserId // Initialise components. cfg := p.getConfiguration() if cfg == nil { // Load configuration first. if err := p.OnConfigurationChange(); err != nil { return fmt.Errorf("initial configuration: %w", err) } cfg = p.getConfiguration() } p.ollamaClient = NewOllamaClient(cfg.OllamaURL) p.conversationStore = NewConversationStore(p.API) p.rateLimiter = NewRateLimiter(cfg.RateLimitPerMinute) return nil } // OnDeactivate is invoked when the plugin is deactivated. func (p *Plugin) OnDeactivate() error { // The bot account persists across restarts — it will be re-used // on next OnActivate. No explicit cleanup needed. return nil } // Implemented returns the list of hooks this plugin handles. func (p *Plugin) Implemented() ([]string, error) { return []string{ "OnActivate", "OnDeactivate", "OnConfigurationChange", "ExecuteCommand", "MessageHasBeenPosted", }, nil } // MessageHasBeenPosted is the primary entry point for @-mention interaction. func (p *Plugin) MessageHasBeenPosted(c *plugin.Context, post *model.Post) { // Never respond to our own posts. if post.UserId == p.botUserID { return } // Never respond to other bots. if post.IsFromOAuthBot() { return } // Check if this is a thread reply to a post we made. threadOwned := false if post.RootId != "" { rootPost, appErr := p.API.GetPost(post.RootId) if appErr == nil && rootPost.UserId == p.botUserID { threadOwned = true } } // Check if the bot is @-mentioned in the message. mentioned := strings.Contains( strings.ToLower(post.Message), "@"+botUsername, ) // If not mentioned and not a thread reply, ignore. if !mentioned && !threadOwned { return } // Build the prompt by stripping the @-mention. prompt := post.Message if mentioned { prompt = strings.TrimSpace(strings.Replace( strings.ToLower(prompt), "@"+botUsername, "", 1, )) } // Handle stop words before sending to Ollama. cfg := p.getConfiguration() if stopWordMatch(prompt, cfg.stopWordSet()) { p.postReply(post, "Goodbye! Feel free to @mention me again anytime.") return } // Authorisation check. if users := cfg.allowedUsers(); users != nil && !users[post.UserId] { p.postReply(post, "You are not authorised to use this bot.") return } // Rate limit check. if !p.rateLimiter.Allow(post.UserId) { p.postReply(post, "Rate limit exceeded. Please wait before sending another request.") return } // Build conversation context. messages, err := p.conversationStore.BuildMessages(post.UserId, post.ChannelId, prompt) if err != nil { p.postReply(post, "Failed to build conversation context.") return } // Send to Ollama. resp, err := p.ollamaClient.ChatCompletion(&ChatRequest{ Model: cfg.DefaultModel, Messages: messages, Options: map[string]any{}, }) if err != nil { p.postReply(post, fmt.Sprintf("Failed to get response from Ollama: %v", err)) return } p.postReply(post, resp.Message.Content) } // postReply creates a reply post in the same thread as the given post. func (p *Plugin) postReply(replyTo *model.Post, text string) { replyPost := &model.Post{ UserId: p.botUserID, ChannelId: replyTo.ChannelId, Message: text, RootId: replyTo.RootId, } // If the original post has no root, use the original post ID as root. if replyPost.RootId == "" { replyPost.RootId = replyTo.Id } if _, appErr := p.API.CreatePost(replyPost); appErr != nil { // Log the failure but don't crash. p.API.LogError("Failed to create reply post", "error", appErr.Error()) } }