279 lines
7.0 KiB
Go
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.sentry" + "/dialog/submit",
|
|
Dialog: *modal,
|
|
}
|
|
|
|
if appErr := p.API.OpenInteractiveDialog(req); appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
return &model.CommandResponse{}, nil
|
|
}
|