2025-12-26 17:38:20 +03:00

279 lines
7.0 KiB
Go

package plugin
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/mattermost/mattermost/server/public/model"
)
func (p *Plugin) fetchSentryProjectID(projectSlug string) (string, error) {
cfg := p.GetConfiguration()
if cfg.SentryUrl == "" || cfg.SentryOrganisationName == "" || cfg.SentryAuthToken == "" {
return "", errors.New("sentry is not configured")
}
url := fmt.Sprintf(
"%s/api/0/projects/%s/%s/",
strings.TrimRight(cfg.SentryUrl, "/"),
cfg.SentryOrganisationName,
projectSlug,
)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+cfg.SentryAuthToken)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("sentry api error (%d): %s", resp.StatusCode, string(body))
}
var result struct {
ID string `json:"id"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", err
}
if result.ID == "" {
return "", errors.New("project id not found")
}
return result.ID, nil
}
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 <project_slug>",
}, nil
}
cfg := p.GetConfiguration()
if cfg.SentryUrl == "" || cfg.SentryOrganisationName == "" || cfg.SentryAuthToken == "" {
return &model.CommandResponse{
ResponseType: model.CommandResponseTypeEphemeral,
Text: "❌ Sentry is not configured. Please fill plugin settings.",
}, nil
}
projectSlug := split[2]
channelID := args.ChannelId
// fetch project id from Sentry
projectID, err := p.fetchSentryProjectID(projectSlug)
if err != nil {
p.API.LogError("Failed to fetch Sentry project", "project", projectSlug, "err", err.Error())
return &model.CommandResponse{
ResponseType: model.CommandResponseTypeEphemeral,
Text: "❌ Failed to fetch Sentry project: " + err.Error(),
}, nil
}
// projectID -> channel
if err := p.API.KVSet("sentry:project:"+projectID, []byte(channelID)); err != nil {
return &model.CommandResponse{
ResponseType: model.CommandResponseTypeEphemeral,
Text: "❌ Failed to save project mapping",
}, nil
}
// store list of linked projects
data, _ := p.API.KVGet("sentry:projects")
var projects []string
if data != nil {
projects = strings.Split(string(data), ",")
}
found := false
for _, id := range projects {
if id == projectID {
found = true
break
}
}
if !found {
projects = append(projects, projectID)
}
_ = p.API.KVSet("sentry:projects", []byte(strings.Join(projects, ",")))
p.API.CreatePost(&model.Post{
UserId: p.botUserID,
ChannelId: channelID,
Message: fmt.Sprintf(
"✅ Sentry project `%s` (id: %s) linked to this channel",
projectSlug,
projectID,
),
})
return &model.CommandResponse{
ResponseType: model.CommandResponseTypeEphemeral,
Text: "🔗 Linked project `" + projectSlug + "`",
}, 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 <project_slug>",
}, nil
}
project := split[2]
// удаляем привязку project -> channel
p.API.KVDelete("sentry:project:" + project)
// обновляем список проектов
data, _ := p.API.KVGet("sentry:projects")
var projects []string
if data != nil {
projects = strings.Split(string(data), ",")
}
var newProjects []string
for _, p := range projects {
if p != project {
newProjects = append(newProjects, p)
}
}
p.API.KVSet("sentry:projects", []byte(strings.Join(newProjects, ",")))
return &model.CommandResponse{
ResponseType: model.CommandResponseTypeEphemeral,
Text: "✅ Unlinked project `" + project + "`",
}, nil
}
func (p *Plugin) commandList(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
// Для MVP мы перебираем все известные ключи в KV
// Так как KVListKeys нет, можно завести slice всех проектов,
// которые были когда-либо linked через /sentry link
// Эти проекты мы храним отдельно в ключе "sentry:projects"
data, _ := p.API.KVGet("sentry:projects")
var projects []string
if data != nil {
projects = strings.Split(string(data), ",")
}
if len(projects) == 0 {
return &model.CommandResponse{
ResponseType: model.CommandResponseTypeEphemeral,
Text: "_No linked Sentry projects_",
}, nil
}
var lines []string
for _, project := range projects {
channelData, _ := p.API.KVGet("sentry:project:" + project)
if channelData == nil {
continue
}
channelID := string(channelData)
channel, err := p.API.GetChannel(channelID)
channelName := channelID
if err == nil && channel != nil {
channelName = "~" + channel.Name
}
lines = append(lines, "• "+project+" → "+channelName)
}
text := "🔗 **Linked Sentry projects:**\n\n" + strings.Join(lines, "\n")
return &model.CommandResponse{
ResponseType: model.CommandResponseTypeEphemeral,
Text: text,
}, nil
}
func (p *Plugin) commandHelp() *model.CommandResponse {
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) {
if args.TriggerId == "" {
return &model.CommandResponse{
ResponseType: model.CommandResponseTypeEphemeral,
Text: "This command must be run from Mattermost UI",
}, nil
}
channels, _ := p.API.GetChannelsForTeamForUser(
args.TeamId,
args.UserId,
false,
)
options := []*model.PostActionOptions{}
for _, ch := range channels {
options = append(options, &model.PostActionOptions{
Text: ch.DisplayName,
Value: ch.Id,
})
}
modal := &model.Dialog{
Title: "Sentry Setup",
CallbackId: "sentry_setup",
SubmitLabel: "Save",
Elements: []model.DialogElement{
{
DisplayName: "Channel",
Name: "channel_id",
Type: "select",
Options: options,
Default: args.ChannelId,
},
{
DisplayName: "Sentry Project",
Name: "project",
Type: "text",
},
},
}
req := model.OpenDialogRequest{
TriggerId: args.TriggerId,
URL: "/plugins/" + "ru.loop.plugin.template" + "/dialog/submit",
Dialog: *modal,
}
if appErr := p.API.OpenInteractiveDialog(req); appErr != nil {
return nil, appErr
}
return &model.CommandResponse{}, nil
}