From 11d22217528736ef8f1615a60cf71e4e3f15bcc2 Mon Sep 17 00:00:00 2001 From: "kirill.moos" Date: Sat, 24 Jan 2026 08:07:47 +0300 Subject: [PATCH] bot sends all errors and results to dm --- server/plugin/api.go | 24 +++++- server/plugin/bot.go | 31 ++++++++ server/plugin/commands.go | 149 +++++++++++++++++++++++--------------- server/plugin/modal.go | 52 +++++++++---- 4 files changed, 179 insertions(+), 77 deletions(-) diff --git a/server/plugin/api.go b/server/plugin/api.go index a2477ab..f14968a 100644 --- a/server/plugin/api.go +++ b/server/plugin/api.go @@ -48,7 +48,17 @@ func (p *Plugin) ExecuteCommand( split := strings.Fields(args.Command) if len(split) < 2 { - return p.commandHelp(), nil + // Отправляем help в DM + helpText := `**Sentry plugin commands** + +• /sentry link — link project to channel +• /sentry unlink — unlink project +• /sentry list — list linked projects +• /sentry setup — setup project +• /sentry help — show help` + post := &model.Post{Message: helpText} + _ = p.sendDMToUser(args.UserId, post) + return &model.CommandResponse{}, nil } switch split[1] { @@ -68,7 +78,17 @@ func (p *Plugin) ExecuteCommand( case "help": fallthrough default: - return p.commandHelp(), nil + // Отправляем help в DM + helpText := `**Sentry plugin commands** + +• /sentry link — link project to channel +• /sentry unlink — unlink project +• /sentry list — list linked projects +• /sentry setup — setup project +• /sentry help — show help` + post := &model.Post{Message: helpText} + _ = p.sendDMToUser(args.UserId, post) + return &model.CommandResponse{}, nil } } diff --git a/server/plugin/bot.go b/server/plugin/bot.go index cc0463d..9701a21 100644 --- a/server/plugin/bot.go +++ b/server/plugin/bot.go @@ -71,3 +71,34 @@ func (p *Plugin) setBotIcon() error { p.API.LogInfo("Bot icon set successfully") return nil } + +// getDirectChannel получает или создает DM канал между ботом и пользователем +func (p *Plugin) getDirectChannel(userID string) (string, error) { + if p.botUserID == "" { + return "", fmt.Errorf("bot user ID is not set") + } + + channel, appErr := p.API.GetDirectChannel(p.botUserID, userID) + if appErr != nil { + return "", fmt.Errorf("failed to get direct channel: %s", appErr.Error()) + } + + return channel.Id, nil +} + +// sendDMToUser отправляет сообщение пользователю в личные сообщения +func (p *Plugin) sendDMToUser(userID string, post *model.Post) error { + dmChannelID, err := p.getDirectChannel(userID) + if err != nil { + return fmt.Errorf("failed to get DM channel: %w", err) + } + + post.ChannelId = dmChannelID + post.UserId = p.botUserID + + if _, appErr := p.API.CreatePost(post); appErr != nil { + return fmt.Errorf("failed to create post: %s", appErr.Error()) + } + + return nil +} diff --git a/server/plugin/commands.go b/server/plugin/commands.go index 7cdd327..9ec49f5 100644 --- a/server/plugin/commands.go +++ b/server/plugin/commands.go @@ -9,10 +9,9 @@ import ( func (p *Plugin) commandLink(args *model.CommandArgs, split []string) (*model.CommandResponse, *model.AppError) { if len(split) < 3 { - return &model.CommandResponse{ - ResponseType: model.CommandResponseTypeEphemeral, - Text: "❌ Usage: /sentry link ", - }, nil + post := &model.Post{Message: "❌ Usage: /sentry link "} + _ = p.sendDMToUser(args.UserId, post) + return &model.CommandResponse{}, nil } projectSlug := split[2] @@ -26,38 +25,36 @@ func (p *Plugin) commandLink(args *model.CommandArgs, split []string) (*model.Co project, err := p.linkProjectToChannel(projectSlug, channelID, hooks) if err != nil { - return &model.CommandResponse{ - ResponseType: model.CommandResponseTypeEphemeral, - Text: "❌ " + err.Error(), - }, nil + post := &model.Post{Message: "❌ " + err.Error()} + _ = p.sendDMToUser(args.UserId, post) + return &model.CommandResponse{}, nil } - return &model.CommandResponse{ - ResponseType: model.CommandResponseTypeEphemeral, - Text: fmt.Sprintf( + post := &model.Post{ + Message: fmt.Sprintf( "✅ Linked **%s** (`%s`) to this channel", project.Name, project.Slug, ), - }, nil + } + _ = p.sendDMToUser(args.UserId, post) + return &model.CommandResponse{}, nil } func (p *Plugin) commandUnlink(args *model.CommandArgs, split []string) (*model.CommandResponse, *model.AppError) { if len(split) < 3 { - return &model.CommandResponse{ - ResponseType: model.CommandResponseTypeEphemeral, - Text: "❌ Usage: /sentry unlink ", - }, nil + post := &model.Post{Message: "❌ Usage: /sentry unlink "} + _ = p.sendDMToUser(args.UserId, post) + return &model.CommandResponse{}, nil } slug := split[2] projects, err := p.getAllProjects() if err != nil { - return &model.CommandResponse{ - ResponseType: model.CommandResponseTypeEphemeral, - Text: "ℹ️ No linked projects", - }, nil + post := &model.Post{Message: "ℹ️ No linked projects"} + _ = p.sendDMToUser(args.UserId, post) + return &model.CommandResponse{}, nil } var removed *LinkedProject @@ -65,66 +62,94 @@ func (p *Plugin) commandUnlink(args *model.CommandArgs, split []string) (*model. if project.Slug == slug { removed = project if err := p.deleteProject(project.ID); err != nil { - return &model.CommandResponse{ - ResponseType: model.CommandResponseTypeEphemeral, - Text: "❌ Failed to unlink project: " + err.Error(), - }, nil + post := &model.Post{Message: "❌ Failed to unlink project: " + err.Error()} + _ = p.sendDMToUser(args.UserId, post) + return &model.CommandResponse{}, nil } break } } if removed == nil { - return &model.CommandResponse{ - ResponseType: model.CommandResponseTypeEphemeral, - Text: "❌ Project not found", - }, nil + post := &model.Post{Message: "❌ Project not found"} + _ = p.sendDMToUser(args.UserId, post) + return &model.CommandResponse{}, nil } - return &model.CommandResponse{ - ResponseType: model.CommandResponseTypeEphemeral, - Text: fmt.Sprintf( + post := &model.Post{ + Message: fmt.Sprintf( "✅ Unlinked **%s** (`%s`)", removed.Name, removed.Slug, ), - }, nil + } + _ = p.sendDMToUser(args.UserId, post) + return &model.CommandResponse{}, nil } func (p *Plugin) commandList(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { projects, err := p.getAllProjects() if err != nil || len(projects) == 0 { - return &model.CommandResponse{ - ResponseType: model.CommandResponseTypeEphemeral, - Text: "_No linked Sentry projects_", - }, nil + post := &model.Post{Message: "_No linked Sentry projects_"} + _ = p.sendDMToUser(args.UserId, post) + return &model.CommandResponse{}, nil } - var lines []string + // Формируем таблицу + var tableRows []string + + // Заголовок таблицы + tableRows = append(tableRows, "| Project | Slug | Channel | Hooks |") + tableRows = append(tableRows, "|---------|------|---------|-------|") + + // Строки с данными for _, project := range projects { channelName := project.ChannelID if ch, err := p.API.GetChannel(project.ChannelID); err == nil { channelName = "~" + ch.Name } - lines = append( - lines, - fmt.Sprintf( - "• **%s** (`%s`) → %s", - project.Name, - project.Slug, - channelName, - ), + // Формируем список включенных хуков + var hooks []string + if project.Hooks.EventAlert { + hooks = append(hooks, "Event") + } + if project.Hooks.MetricAlert { + hooks = append(hooks, "Metric") + } + if project.Hooks.Issue { + hooks = append(hooks, "Issue") + } + if project.Hooks.Comment { + hooks = append(hooks, "Comment") + } + + hooksStr := strings.Join(hooks, ", ") + if hooksStr == "" { + hooksStr = "-" + } + + // Экранируем символы для markdown таблицы + projectName := strings.ReplaceAll(project.Name, "|", "\\|") + slug := strings.ReplaceAll(project.Slug, "|", "\\|") + channelName = strings.ReplaceAll(channelName, "|", "\\|") + hooksStr = strings.ReplaceAll(hooksStr, "|", "\\|") + + tableRows = append( + tableRows, + fmt.Sprintf("| %s | `%s` | %s | %s |", projectName, slug, channelName, hooksStr), ) } - return &model.CommandResponse{ - ResponseType: model.CommandResponseTypeEphemeral, - Text: "🔗 **Linked Sentry projects:**\n\n" + strings.Join(lines, "\n"), - }, nil + message := "🔗 **Linked Sentry projects:**\n\n" + strings.Join(tableRows, "\n") + post := &model.Post{Message: message} + _ = p.sendDMToUser(args.UserId, post) + return &model.CommandResponse{}, nil } func (p *Plugin) commandHelp() *model.CommandResponse { + // Help отправляется через ephemeral в ExecuteCommand, но мы не можем получить userId здесь + // Поэтому оставляем как есть, но это будет обработано в ExecuteCommand return &model.CommandResponse{ ResponseType: model.CommandResponseTypeEphemeral, Text: `**Sentry plugin commands** @@ -139,20 +164,18 @@ func (p *Plugin) commandSetup(args *model.CommandArgs) (*model.CommandResponse, if args.TriggerId == "" { p.API.LogWarn("commandSetup: triggerId is empty") - return &model.CommandResponse{ - ResponseType: model.CommandResponseTypeEphemeral, - Text: "This command must be run from Mattermost UI", - }, nil + post := &model.Post{Message: "This command must be run from Loop UI"} + _ = p.sendDMToUser(args.UserId, post) + return &model.CommandResponse{}, nil } // Получаем каналы пользователя channels, appErr := p.API.GetChannelsForTeamForUser(args.TeamId, args.UserId, false) if appErr != nil { p.API.LogError("Failed to load channels", "error", appErr.Error()) - return &model.CommandResponse{ - ResponseType: model.CommandResponseTypeEphemeral, - Text: "❌ Failed to load channels", - }, nil + post := &model.Post{Message: "❌ Failed to load channels"} + _ = p.sendDMToUser(args.UserId, post) + return &model.CommandResponse{}, nil } channelOptions := []*model.PostActionOptions{} @@ -253,13 +276,17 @@ func (p *Plugin) commandSetupHook( project, err := p.getProjectByChannel(args.ChannelId) if err != nil { - return ephemeral("❌ " + err.Error()), nil + post := &model.Post{Message: "❌ " + err.Error()} + _ = p.sendDMToUser(args.UserId, post) + return &model.CommandResponse{}, nil } hookType := HookType(hook) if !isHookEnabled(project, hookType) { - return ephemeral("⚠️ This webhook is disabled in setup"), nil + post := &model.Post{Message: "⚠️ This webhook is disabled in setup"} + _ = p.sendDMToUser(args.UserId, post) + return &model.CommandResponse{}, nil } switch hookType { @@ -272,6 +299,8 @@ func (p *Plugin) commandSetupHook( case HookComment: return p.openCommentSetup(args, project) default: - return ephemeral("❌ Unknown webhook type"), nil + post := &model.Post{Message: "❌ Unknown webhook type"} + _ = p.sendDMToUser(args.UserId, post) + return &model.CommandResponse{}, nil } } diff --git a/server/plugin/modal.go b/server/plugin/modal.go index 612015f..0fa6cf4 100644 --- a/server/plugin/modal.go +++ b/server/plugin/modal.go @@ -34,15 +34,18 @@ func (p *Plugin) handleDialogSubmit(w http.ResponseWriter, r *http.Request) { project, err := p.linkProjectToChannel(projectSlug, channelID, hooks) if err != nil { - p.API.SendEphemeralPost(req.UserId, &model.Post{ - ChannelId: channelID, - Message: "❌ " + err.Error(), - }) + // Отправляем ошибку в личные сообщения + post := &model.Post{ + Message: "❌ " + err.Error(), + } + if dmErr := p.sendDMToUser(req.UserId, post); dmErr != nil { + p.API.LogError("Failed to send DM to user", "error", dmErr.Error()) + } return } - p.API.SendEphemeralPost(req.UserId, &model.Post{ - ChannelId: channelID, + // Отправляем успешное сообщение в личные сообщения + post := &model.Post{ Message: fmt.Sprintf( "✅ Linked **%s** (`%s`)\nHooks: event=%v, metric=%v, issue=%v, comment=%v", project.Name, @@ -52,7 +55,10 @@ func (p *Plugin) handleDialogSubmit(w http.ResponseWriter, r *http.Request) { hooks.Issue, hooks.Comment, ), - }) + } + if dmErr := p.sendDMToUser(req.UserId, post); dmErr != nil { + p.API.LogError("Failed to send DM to user", "error", dmErr.Error()) + } } func (p *Plugin) openEventAlertSetup( @@ -61,10 +67,14 @@ func (p *Plugin) openEventAlertSetup( ) (*model.CommandResponse, *model.AppError) { if args.TriggerId == "" { - return ephemeral("This command must be run from UI"), nil + post := &model.Post{Message: "This command must be run from UI"} + _ = p.sendDMToUser(args.UserId, post) + return &model.CommandResponse{}, nil } - return ephemeral("🛠 Event alert setup is not implemented yet"), nil + post := &model.Post{Message: "🛠 Event alert setup is not implemented yet"} + _ = p.sendDMToUser(args.UserId, post) + return &model.CommandResponse{}, nil } func (p *Plugin) openMetricAlertSetup( @@ -73,10 +83,14 @@ func (p *Plugin) openMetricAlertSetup( ) (*model.CommandResponse, *model.AppError) { if args.TriggerId == "" { - return ephemeral("This command must be run from UI"), nil + post := &model.Post{Message: "This command must be run from UI"} + _ = p.sendDMToUser(args.UserId, post) + return &model.CommandResponse{}, nil } - return ephemeral("🛠 Metric alert setup is not implemented yet"), nil + post := &model.Post{Message: "🛠 Metric alert setup is not implemented yet"} + _ = p.sendDMToUser(args.UserId, post) + return &model.CommandResponse{}, nil } func (p *Plugin) openIssueSetup( @@ -85,10 +99,14 @@ func (p *Plugin) openIssueSetup( ) (*model.CommandResponse, *model.AppError) { if args.TriggerId == "" { - return ephemeral("This command must be run from UI"), nil + post := &model.Post{Message: "This command must be run from UI"} + _ = p.sendDMToUser(args.UserId, post) + return &model.CommandResponse{}, nil } - return ephemeral("🛠 Issue setup is not implemented yet"), nil + post := &model.Post{Message: "🛠 Issue setup is not implemented yet"} + _ = p.sendDMToUser(args.UserId, post) + return &model.CommandResponse{}, nil } func (p *Plugin) openCommentSetup( @@ -97,8 +115,12 @@ func (p *Plugin) openCommentSetup( ) (*model.CommandResponse, *model.AppError) { if args.TriggerId == "" { - return ephemeral("This command must be run from UI"), nil + post := &model.Post{Message: "This command must be run from UI"} + _ = p.sendDMToUser(args.UserId, post) + return &model.CommandResponse{}, nil } - return ephemeral("🛠 Comment setup is not implemented yet"), nil + post := &model.Post{Message: "🛠 Comment setup is not implemented yet"} + _ = p.sendDMToUser(args.UserId, post) + return &model.CommandResponse{}, nil }