From 4fabac802165a104cd043f48414458b45a3dd88e Mon Sep 17 00:00:00 2001 From: Zachary Tsang Date: Thu, 15 Aug 2024 10:48:54 +0800 Subject: [PATCH] Install MessageFilter in app and notifier --- pkg/slack/app.go | 33 +++--- pkg/slack/command.go | 244 ++++++++++++++++++++++++++++++++++++++++++ pkg/slack/notifier.go | 3 +- 3 files changed, 266 insertions(+), 14 deletions(-) create mode 100644 pkg/slack/command.go diff --git a/pkg/slack/app.go b/pkg/slack/app.go index 4314529..306159e 100644 --- a/pkg/slack/app.go +++ b/pkg/slack/app.go @@ -27,26 +27,26 @@ type App struct { } type ChannelInfo struct { - channelID string - conclusions []string + channelID string + filter *MessageFilter } type exposedChannelInfo struct { - ChannelID string `json:"channelID"` - Conclusions []string `json:"messageFilters"` + ChannelID string `json:"channelID"` + Filter *MessageFilter `json:"messageFilters"` } func (f ChannelInfo) String() string { - if len(f.conclusions) == 0 { + if f.filter.Length() == 0 { return f.channelID } - return fmt.Sprintf("%s with %s", f.channelID, f.conclusions) + return fmt.Sprintf("%s with %s", f.channelID, f.filter) } func (f ChannelInfo) MarshalJSON() ([]byte, error) { return json.Marshal(exposedChannelInfo{ - ChannelID: f.channelID, - Conclusions: f.conclusions, + ChannelID: f.channelID, + Filter: f.filter, }) } @@ -56,7 +56,7 @@ func (f *ChannelInfo) UnmarshalJSON(data []byte) error { return err } f.channelID = aux.ChannelID - f.conclusions = aux.Conclusions + f.filter = aux.Filter return nil } @@ -225,11 +225,20 @@ func (a *App) messageLoop(ctx context.Context, client *socketmode.Client) { switch subcommand { case "subscribe": + filterLayers := array.Unique(args[2:]) + filter, err := NewFilter(filterLayers) + if err != nil { + a.logger.Warn("failed to subscribe", zap.Error(err)) + client.Ack(*e.Request, map[string]interface{}{ + "text": fmt.Sprintf("Failed to subscribe '%s': %s\n", repo, err), + }) + } + channelInfo := ChannelInfo{ - channelID: data.ChannelID, - conclusions: conclusions, + channelID: data.ChannelID, + filter: filter, } - err := a.AddChannel(ctx, repo, channelInfo) + err = a.AddChannel(ctx, repo, channelInfo) if err != nil { a.logger.Warn("failed to subscribe", zap.Error(err)) client.Ack(*e.Request, map[string]interface{}{ diff --git a/pkg/slack/command.go b/pkg/slack/command.go new file mode 100644 index 0000000..4ebf366 --- /dev/null +++ b/pkg/slack/command.go @@ -0,0 +1,244 @@ +package slack + +import ( + "context" + "fmt" + "strings" + + "github.com/oursky/github-actions-manager/pkg/utils/array" + "github.com/slack-go/slack" + "go.uber.org/zap" +) + +type Argument struct { + name string + required bool + acceptsMany bool + description string +} + +func NewArgument(name string, required bool, acceptsMany bool, description string) Argument { + return Argument{name: name, required: required, acceptsMany: acceptsMany, description: description} +} + +type Command struct { + trigger string + arguments []Argument + description string + execute func(env *CLI, args []string) CLIResult +} + +type CLIResult struct { + printToChannel bool + message string +} + +func NewCLIResult(printToChannel bool, message string) CLIResult { + return CLIResult{printToChannel: printToChannel, message: message} +} + +type CLI struct { + commands *[]Command + app *App + ctx context.Context + channelID string + logger *zap.Logger +} + +func (c Command) String() string { + output := fmt.Sprintf("`%s`: %s", c.trigger, c.description) + output += fmt.Sprintf("\nUsage of `%s`: `%s", c.trigger, c.trigger) + for _, arg := range c.arguments { + argname := arg.name + if arg.acceptsMany { + argname += "..." + } + if arg.required { + output += " " + argname + } else { + output += " [" + argname + "]" + } + } + output += "`" + for _, arg := range c.arguments { + output += fmt.Sprintf("\n\t`%s`: %s", arg.name, arg.description) + } + + return output +} + +func (cli *CLI) SetContext(ctx context.Context, a *App) { + cli.app = a + cli.ctx = ctx +} + +func (cli *CLI) Execute(subcommand string, args []string) CLIResult { + commands := *cli.commands + for _, command := range commands { + if subcommand != command.trigger { + continue + } + return command.execute( + cli, + args, + ) + } + return NewCLIResult(false, fmt.Sprintf("Unknown command: %s", subcommand)) +} + +func (cli *CLI) Parse(data slack.SlashCommand) map[string]interface{} { + cli.channelID = data.ChannelID + + if data.Command != "/"+cli.app.commandName { + return map[string]interface{}{"text": fmt.Sprintf("Unknown command '%s'\n", data.Command)} + } + + args := strings.Split(data.Text, " ") + if len(args) < 1 { + return map[string]interface{}{"text": fmt.Sprintf("Please specify subcommand")} + } + + result := cli.Execute(args[0], args[1:]) + response := map[string]interface{}{"text": result.message} + if result.printToChannel { + response["response_type"] = "in_channel" + } + return response +} + +func NewCLI(logger *zap.Logger) *CLI { + cli := CLI{ + commands: &[]Command{}, + logger: logger, + } + cli.commands = &[]Command{ + { + trigger: "help", + arguments: []Argument{ + NewArgument("subcommand", false, false, "The subcommand to get help about."), + }, + description: "Get help about a command.", + execute: func(env *CLI, args []string) CLIResult { + output := "" + commands := (*cli.commands) + if len(args) == 0 { + output = "The known commands are:" + for _, command := range commands { + output += fmt.Sprintf(" `%s`", command.trigger) + } + return NewCLIResult(false, output) + } + subcommand := args[0] + for _, command := range commands { + if command.trigger != subcommand { + continue + } + return NewCLIResult(false, command.String()) + } + return NewCLIResult(false, fmt.Sprintf("No such command: %s", subcommand)) + }, + }, + { + trigger: "list", + arguments: []Argument{ + NewArgument("repo", true, false, "The repo to get the subscription data for."), + }, + description: "List the channels subscribed to a given repo.", + execute: func(env *CLI, args []string) CLIResult { + if len(args) < 1 { + return NewCLIResult(false, "Please specify repo") + } + + repo := args[0] + if !repoRegex.MatchString(repo) { + return NewCLIResult(false, fmt.Sprintf("Invalid repo *%s*\n", repo)) + } + + channels, err := env.app.GetChannels(env.ctx, repo) + if err != nil { + env.logger.Warn("failed to list channels", zap.Error(err)) + return NewCLIResult(false, fmt.Sprintf("Failed to get list of subscribed channels: '%s'", err)) + } else { + if len(channels) == 0 { + return NewCLIResult(true, fmt.Sprintf("*%s* is sending updates to no channels", repo)) + } + channelStrings := array.Cast(channels) + return NewCLIResult(true, fmt.Sprintf("*%s* is sending updates to: %s\n", repo, strings.Join(channelStrings, "; "))) + } + }, + }, + { + trigger: "subscribe", + arguments: []Argument{ + NewArgument("repo", true, false, "The repo to subscribe to."), + NewArgument("filters", false, true, "In the format of filter_key:value1,value2,...:conclusion1,conclusion2,..., one of the supported filter keys (workflows, branches)"), + }, + description: "Subscribe this channel to a given repo.", + execute: func(env *CLI, args []string) CLIResult { + if len(args) < 1 { + return NewCLIResult(false, fmt.Sprintf("Please specify repo")) + } + + repo := args[0] + if !repoRegex.MatchString(repo) { + return NewCLIResult(false, fmt.Sprintf("Invalid repo *%s*\n", repo)) + } + + filterLayers := array.Unique(args[1:]) + filter, err := NewFilter(filterLayers) + if err != nil { + env.logger.Warn("failed to subscribe", zap.Error(err)) + return NewCLIResult(false, fmt.Sprintf("Failed to subscribe to *%s*: '%s'\n", repo, err)) + } + + channelInfo := ChannelInfo{ + channelID: env.channelID, + filter: filter, + } + err = env.app.AddChannel(env.ctx, repo, channelInfo) + if err != nil { + env.logger.Warn("failed to subscribe", zap.Error(err)) + return NewCLIResult(false, fmt.Sprintf("Failed to subscribe to *%s*: '%s'\n", repo, err)) + } + if len(filterLayers) > 0 { + return NewCLIResult(true, fmt.Sprintf("Subscribed to *%s* with filter layers %s", repo, filter.whitelists)) + } else { + return NewCLIResult(true, fmt.Sprintf("Subscribed to *%s*\n", repo)) + } + }, + }, + { + trigger: "unsubscribe", + arguments: []Argument{ + NewArgument("repo", true, false, "The repo to unsubscribe from."), + }, + description: "Unsubscribe this channel from a given repo.", + execute: func(env *CLI, args []string) CLIResult { + if len(args) < 1 { + return NewCLIResult(false, fmt.Sprintf("Please specify repo")) + } + + repo := args[0] + if !repoRegex.MatchString(repo) { + return NewCLIResult(false, fmt.Sprintf("Invalid repo *%s*\n", repo)) + } + + err := env.app.DelChannel(env.ctx, repo, env.channelID) + if err != nil { + env.logger.Warn("failed to unsubscribe", zap.Error(err)) + return NewCLIResult(false, fmt.Sprintf("Failed to unsubscribe from *%s*: '%s'\n", repo, err)) + } else { + return NewCLIResult(true, fmt.Sprintf("Unsubscribed from *%s*\n", repo)) + } + }, + }, + { + trigger: "meow", + description: "Meow.", + execute: func(env *CLI, args []string) CLIResult { + return NewCLIResult(false, "meow") + }, + }, + } + return &cli +} diff --git a/pkg/slack/notifier.go b/pkg/slack/notifier.go index 7889429..083a929 100644 --- a/pkg/slack/notifier.go +++ b/pkg/slack/notifier.go @@ -12,7 +12,6 @@ import ( "github.com/slack-go/slack/slackutilsx" "go.uber.org/zap" "golang.org/x/sync/errgroup" - "k8s.io/utils/strings/slices" ) type JobsState interface { @@ -159,7 +158,7 @@ func (n *Notifier) notify(ctx context.Context, run *jobs.WorkflowRun) { } for _, channel := range channels { - if len(channel.conclusions) > 0 && !slices.Contains(channel.conclusions, run.Conclusion) { + if !channel.filter.Any(run) { return } err := n.app.SendMessage(ctx, channel.channelID, slack.MsgOptionAttachments(slackMsg))