bot sends all errors and results to dm

This commit is contained in:
Кирилл Моос 2026-01-24 08:07:47 +03:00
parent 2d5cf75f52
commit 11d2221752
4 changed files with 179 additions and 77 deletions

View File

@ -48,7 +48,17 @@ func (p *Plugin) ExecuteCommand(
split := strings.Fields(args.Command) split := strings.Fields(args.Command)
if len(split) < 2 { if len(split) < 2 {
return p.commandHelp(), nil // Отправляем help в DM
helpText := `**Sentry plugin commands**
/sentry link <project_slug> link project to channel
/sentry unlink <project_slug> 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] { switch split[1] {
@ -68,7 +78,17 @@ func (p *Plugin) ExecuteCommand(
case "help": case "help":
fallthrough fallthrough
default: default:
return p.commandHelp(), nil // Отправляем help в DM
helpText := `**Sentry plugin commands**
/sentry link <project_slug> link project to channel
/sentry unlink <project_slug> 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
} }
} }

View File

@ -71,3 +71,34 @@ func (p *Plugin) setBotIcon() error {
p.API.LogInfo("Bot icon set successfully") p.API.LogInfo("Bot icon set successfully")
return nil 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
}

View File

@ -9,10 +9,9 @@ import (
func (p *Plugin) commandLink(args *model.CommandArgs, split []string) (*model.CommandResponse, *model.AppError) { func (p *Plugin) commandLink(args *model.CommandArgs, split []string) (*model.CommandResponse, *model.AppError) {
if len(split) < 3 { if len(split) < 3 {
return &model.CommandResponse{ post := &model.Post{Message: "❌ Usage: /sentry link <project_slug>"}
ResponseType: model.CommandResponseTypeEphemeral, _ = p.sendDMToUser(args.UserId, post)
Text: "❌ Usage: /sentry link <project_slug>", return &model.CommandResponse{}, nil
}, nil
} }
projectSlug := split[2] 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) project, err := p.linkProjectToChannel(projectSlug, channelID, hooks)
if err != nil { if err != nil {
return &model.CommandResponse{ post := &model.Post{Message: "❌ " + err.Error()}
ResponseType: model.CommandResponseTypeEphemeral, _ = p.sendDMToUser(args.UserId, post)
Text: "❌ " + err.Error(), return &model.CommandResponse{}, nil
}, nil
} }
return &model.CommandResponse{ post := &model.Post{
ResponseType: model.CommandResponseTypeEphemeral, Message: fmt.Sprintf(
Text: fmt.Sprintf(
"✅ Linked **%s** (`%s`) to this channel", "✅ Linked **%s** (`%s`) to this channel",
project.Name, project.Name,
project.Slug, 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) { func (p *Plugin) commandUnlink(args *model.CommandArgs, split []string) (*model.CommandResponse, *model.AppError) {
if len(split) < 3 { if len(split) < 3 {
return &model.CommandResponse{ post := &model.Post{Message: "❌ Usage: /sentry unlink <project_slug>"}
ResponseType: model.CommandResponseTypeEphemeral, _ = p.sendDMToUser(args.UserId, post)
Text: "❌ Usage: /sentry unlink <project_slug>", return &model.CommandResponse{}, nil
}, nil
} }
slug := split[2] slug := split[2]
projects, err := p.getAllProjects() projects, err := p.getAllProjects()
if err != nil { if err != nil {
return &model.CommandResponse{ post := &model.Post{Message: " No linked projects"}
ResponseType: model.CommandResponseTypeEphemeral, _ = p.sendDMToUser(args.UserId, post)
Text: " No linked projects", return &model.CommandResponse{}, nil
}, nil
} }
var removed *LinkedProject var removed *LinkedProject
@ -65,66 +62,94 @@ func (p *Plugin) commandUnlink(args *model.CommandArgs, split []string) (*model.
if project.Slug == slug { if project.Slug == slug {
removed = project removed = project
if err := p.deleteProject(project.ID); err != nil { if err := p.deleteProject(project.ID); err != nil {
return &model.CommandResponse{ post := &model.Post{Message: "❌ Failed to unlink project: " + err.Error()}
ResponseType: model.CommandResponseTypeEphemeral, _ = p.sendDMToUser(args.UserId, post)
Text: "❌ Failed to unlink project: " + err.Error(), return &model.CommandResponse{}, nil
}, nil
} }
break break
} }
} }
if removed == nil { if removed == nil {
return &model.CommandResponse{ post := &model.Post{Message: "❌ Project not found"}
ResponseType: model.CommandResponseTypeEphemeral, _ = p.sendDMToUser(args.UserId, post)
Text: "❌ Project not found", return &model.CommandResponse{}, nil
}, nil
} }
return &model.CommandResponse{ post := &model.Post{
ResponseType: model.CommandResponseTypeEphemeral, Message: fmt.Sprintf(
Text: fmt.Sprintf(
"✅ Unlinked **%s** (`%s`)", "✅ Unlinked **%s** (`%s`)",
removed.Name, removed.Name,
removed.Slug, removed.Slug,
), ),
}, nil }
_ = p.sendDMToUser(args.UserId, post)
return &model.CommandResponse{}, nil
} }
func (p *Plugin) commandList(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { func (p *Plugin) commandList(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
projects, err := p.getAllProjects() projects, err := p.getAllProjects()
if err != nil || len(projects) == 0 { if err != nil || len(projects) == 0 {
return &model.CommandResponse{ post := &model.Post{Message: "_No linked Sentry projects_"}
ResponseType: model.CommandResponseTypeEphemeral, _ = p.sendDMToUser(args.UserId, post)
Text: "_No linked Sentry projects_", return &model.CommandResponse{}, nil
}, nil
} }
var lines []string // Формируем таблицу
var tableRows []string
// Заголовок таблицы
tableRows = append(tableRows, "| Project | Slug | Channel | Hooks |")
tableRows = append(tableRows, "|---------|------|---------|-------|")
// Строки с данными
for _, project := range projects { for _, project := range projects {
channelName := project.ChannelID channelName := project.ChannelID
if ch, err := p.API.GetChannel(project.ChannelID); err == nil { if ch, err := p.API.GetChannel(project.ChannelID); err == nil {
channelName = "~" + ch.Name channelName = "~" + ch.Name
} }
lines = append( // Формируем список включенных хуков
lines, var hooks []string
fmt.Sprintf( if project.Hooks.EventAlert {
"• **%s** (`%s`) → %s", hooks = append(hooks, "Event")
project.Name, }
project.Slug, if project.Hooks.MetricAlert {
channelName, 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{ message := "🔗 **Linked Sentry projects:**\n\n" + strings.Join(tableRows, "\n")
ResponseType: model.CommandResponseTypeEphemeral, post := &model.Post{Message: message}
Text: "🔗 **Linked Sentry projects:**\n\n" + strings.Join(lines, "\n"), _ = p.sendDMToUser(args.UserId, post)
}, nil return &model.CommandResponse{}, nil
} }
func (p *Plugin) commandHelp() *model.CommandResponse { func (p *Plugin) commandHelp() *model.CommandResponse {
// Help отправляется через ephemeral в ExecuteCommand, но мы не можем получить userId здесь
// Поэтому оставляем как есть, но это будет обработано в ExecuteCommand
return &model.CommandResponse{ return &model.CommandResponse{
ResponseType: model.CommandResponseTypeEphemeral, ResponseType: model.CommandResponseTypeEphemeral,
Text: `**Sentry plugin commands** Text: `**Sentry plugin commands**
@ -139,20 +164,18 @@ func (p *Plugin) commandSetup(args *model.CommandArgs) (*model.CommandResponse,
if args.TriggerId == "" { if args.TriggerId == "" {
p.API.LogWarn("commandSetup: triggerId is empty") p.API.LogWarn("commandSetup: triggerId is empty")
return &model.CommandResponse{ post := &model.Post{Message: "This command must be run from Loop UI"}
ResponseType: model.CommandResponseTypeEphemeral, _ = p.sendDMToUser(args.UserId, post)
Text: "This command must be run from Mattermost UI", return &model.CommandResponse{}, nil
}, nil
} }
// Получаем каналы пользователя // Получаем каналы пользователя
channels, appErr := p.API.GetChannelsForTeamForUser(args.TeamId, args.UserId, false) channels, appErr := p.API.GetChannelsForTeamForUser(args.TeamId, args.UserId, false)
if appErr != nil { if appErr != nil {
p.API.LogError("Failed to load channels", "error", appErr.Error()) p.API.LogError("Failed to load channels", "error", appErr.Error())
return &model.CommandResponse{ post := &model.Post{Message: "❌ Failed to load channels"}
ResponseType: model.CommandResponseTypeEphemeral, _ = p.sendDMToUser(args.UserId, post)
Text: "❌ Failed to load channels", return &model.CommandResponse{}, nil
}, nil
} }
channelOptions := []*model.PostActionOptions{} channelOptions := []*model.PostActionOptions{}
@ -253,13 +276,17 @@ func (p *Plugin) commandSetupHook(
project, err := p.getProjectByChannel(args.ChannelId) project, err := p.getProjectByChannel(args.ChannelId)
if err != nil { 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) hookType := HookType(hook)
if !isHookEnabled(project, hookType) { 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 { switch hookType {
@ -272,6 +299,8 @@ func (p *Plugin) commandSetupHook(
case HookComment: case HookComment:
return p.openCommentSetup(args, project) return p.openCommentSetup(args, project)
default: default:
return ephemeral("❌ Unknown webhook type"), nil post := &model.Post{Message: "❌ Unknown webhook type"}
_ = p.sendDMToUser(args.UserId, post)
return &model.CommandResponse{}, nil
} }
} }

View File

@ -34,15 +34,18 @@ func (p *Plugin) handleDialogSubmit(w http.ResponseWriter, r *http.Request) {
project, err := p.linkProjectToChannel(projectSlug, channelID, hooks) project, err := p.linkProjectToChannel(projectSlug, channelID, hooks)
if err != nil { if err != nil {
p.API.SendEphemeralPost(req.UserId, &model.Post{ // Отправляем ошибку в личные сообщения
ChannelId: channelID, post := &model.Post{
Message: "❌ " + err.Error(), 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 return
} }
p.API.SendEphemeralPost(req.UserId, &model.Post{ // Отправляем успешное сообщение в личные сообщения
ChannelId: channelID, post := &model.Post{
Message: fmt.Sprintf( Message: fmt.Sprintf(
"✅ Linked **%s** (`%s`)\nHooks: event=%v, metric=%v, issue=%v, comment=%v", "✅ Linked **%s** (`%s`)\nHooks: event=%v, metric=%v, issue=%v, comment=%v",
project.Name, project.Name,
@ -52,7 +55,10 @@ func (p *Plugin) handleDialogSubmit(w http.ResponseWriter, r *http.Request) {
hooks.Issue, hooks.Issue,
hooks.Comment, 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( func (p *Plugin) openEventAlertSetup(
@ -61,10 +67,14 @@ func (p *Plugin) openEventAlertSetup(
) (*model.CommandResponse, *model.AppError) { ) (*model.CommandResponse, *model.AppError) {
if args.TriggerId == "" { 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( func (p *Plugin) openMetricAlertSetup(
@ -73,10 +83,14 @@ func (p *Plugin) openMetricAlertSetup(
) (*model.CommandResponse, *model.AppError) { ) (*model.CommandResponse, *model.AppError) {
if args.TriggerId == "" { 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( func (p *Plugin) openIssueSetup(
@ -85,10 +99,14 @@ func (p *Plugin) openIssueSetup(
) (*model.CommandResponse, *model.AppError) { ) (*model.CommandResponse, *model.AppError) {
if args.TriggerId == "" { 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( func (p *Plugin) openCommentSetup(
@ -97,8 +115,12 @@ func (p *Plugin) openCommentSetup(
) (*model.CommandResponse, *model.AppError) { ) (*model.CommandResponse, *model.AppError) {
if args.TriggerId == "" { 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
} }