307 lines
9.3 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 <project_slug>"}
_ = 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 <project_slug>"}
_ = 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 <project_slug> — 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
}
}