package plugin import ( "fmt" "strings" "github.com/mattermost/mattermost/server/public/model" ) func (p *Plugin) commandLink(args *model.CommandArgs, split []string) (*model.CommandResponse, *model.AppError) { if len(split) < 3 { post := &model.Post{Message: "❌ Usage: /sentry link "} _ = p.sendDMToUser(args.UserId, post) return &model.CommandResponse{}, nil } projectSlug := split[2] channelID := args.ChannelId hooks := HookSettings{ EventAlert: true, MetricAlert: true, Issue: true, Comment: true, } project, err := p.linkProjectToChannel(projectSlug, channelID, hooks) if err != nil { post := &model.Post{Message: "❌ " + err.Error()} _ = p.sendDMToUser(args.UserId, post) return &model.CommandResponse{}, nil } post := &model.Post{ Message: fmt.Sprintf( "✅ Linked **%s** (`%s`) to this channel", project.Name, project.Slug, ), } _ = 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 { 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 { post := &model.Post{Message: "ℹ️ No linked projects"} _ = p.sendDMToUser(args.UserId, post) return &model.CommandResponse{}, nil } var removed *LinkedProject for _, project := range projects { if project.Slug == slug { removed = project if err := p.deleteProject(project.ID); err != nil { post := &model.Post{Message: "❌ Failed to unlink project: " + err.Error()} _ = p.sendDMToUser(args.UserId, post) return &model.CommandResponse{}, nil } break } } if removed == nil { post := &model.Post{Message: "❌ Project not found"} _ = p.sendDMToUser(args.UserId, post) return &model.CommandResponse{}, nil } post := &model.Post{ Message: fmt.Sprintf( "✅ Unlinked **%s** (`%s`)", removed.Name, removed.Slug, ), } _ = 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 { post := &model.Post{Message: "_No linked Sentry projects_"} _ = p.sendDMToUser(args.UserId, post) return &model.CommandResponse{}, nil } // Формируем таблицу 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 } // Формируем список включенных хуков 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), ) } 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** • /sentry link — link project to channel • /sentry help — show help`, } } func (p *Plugin) commandSetup(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { p.API.LogDebug("commandSetup called", "triggerId", args.TriggerId) if args.TriggerId == "" { p.API.LogWarn("commandSetup: triggerId is empty") 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()) post := &model.Post{Message: "❌ Failed to load channels"} _ = p.sendDMToUser(args.UserId, post) return &model.CommandResponse{}, nil } channelOptions := []*model.PostActionOptions{} for _, ch := range channels { channelOptions = append(channelOptions, &model.PostActionOptions{ Text: ch.DisplayName, Value: ch.Id, }) } // Получаем проекты из Sentry sentryProjects, err := p.fetchSentryProjects() var projectElement model.DialogElement if err != nil { // Если не удалось получить проекты, используем текстовое поле как fallback p.API.LogWarn("Failed to load Sentry projects, using text input", "error", err.Error()) projectElement = model.DialogElement{ DisplayName: "Sentry project slug", Name: "project_slug", Type: "text", Placeholder: "frontend-app", } } else if len(sentryProjects) == 0 { // Если список проектов пустой, используем текстовое поле p.API.LogWarn("No Sentry projects found, using text input") projectElement = model.DialogElement{ DisplayName: "Sentry project slug", Name: "project_slug", Type: "text", Placeholder: "frontend-app", } } else { // Формируем опции для select проектов projectOptions := []*model.PostActionOptions{} for _, proj := range sentryProjects { projectOptions = append(projectOptions, &model.PostActionOptions{ Text: fmt.Sprintf("%s (%s)", proj.Name, proj.Slug), Value: proj.Slug, }) } projectElement = model.DialogElement{ DisplayName: "Sentry project", Name: "project_slug", Type: "select", Options: projectOptions, } p.API.LogDebug("Loaded Sentry projects", "count", len(sentryProjects)) } modal := &model.Dialog{ Title: "Sentry Setup", CallbackId: "sentry_setup", SubmitLabel: "Save", Elements: []model.DialogElement{ // ───── General ───── { DisplayName: "Default channel", Name: "default_channel_id", Type: "select", Options: channelOptions, Default: args.ChannelId, }, projectElement, // ───── Webhook types ───── {DisplayName: "Event alerts", Name: "hook_event_alert", Type: "bool", Default: "true"}, {DisplayName: "Metric alerts", Name: "hook_metric_alert", Type: "bool", Default: "true"}, {DisplayName: "Issues", Name: "hook_issue", Type: "bool", Default: "true"}, {DisplayName: "Comments", Name: "hook_comment", Type: "bool", Default: "true"}, }, } p.API.LogDebug("Opening dialog", "elements_count", len(modal.Elements)) req := model.OpenDialogRequest{ TriggerId: args.TriggerId, URL: "/plugins/ru.loop.plugin.sentry/dialog/submit", Dialog: *modal, } if appErr := p.API.OpenInteractiveDialog(req); appErr != nil { p.API.LogError("Failed to open setup dialog", "error", appErr.Error()) return &model.CommandResponse{ ResponseType: model.CommandResponseTypeEphemeral, Text: "❌ Failed to open setup dialog: " + appErr.Error(), }, nil } p.API.LogDebug("Dialog opened successfully") return &model.CommandResponse{}, nil } func (p *Plugin) commandSetupHook( args *model.CommandArgs, hook string, ) (*model.CommandResponse, *model.AppError) { project, err := p.getProjectByChannel(args.ChannelId) if err != nil { post := &model.Post{Message: "❌ " + err.Error()} _ = p.sendDMToUser(args.UserId, post) return &model.CommandResponse{}, nil } hookType := HookType(hook) if !isHookEnabled(project, hookType) { post := &model.Post{Message: "⚠️ This webhook is disabled in setup"} _ = p.sendDMToUser(args.UserId, post) return &model.CommandResponse{}, nil } switch hookType { case HookEventAlert: return p.openEventAlertSetup(args, project) case HookMetricAlert: return p.openMetricAlertSetup(args, project) case HookIssue: return p.openIssueSetup(args, project) case HookComment: return p.openCommentSetup(args, project) default: post := &model.Post{Message: "❌ Unknown webhook type"} _ = p.sendDMToUser(args.UserId, post) return &model.CommandResponse{}, nil } }