From 38802f6d5ebceea65a27284a5300fb7d234017ec Mon Sep 17 00:00:00 2001 From: Taj Date: Wed, 22 Feb 2023 16:31:19 +0000 Subject: [PATCH 01/28] Sub command layout for working with Slack --- cmd/gh-slack/cmd/read.go | 199 ++++++++++++++++++ .../{main_test.go => cmd/read_test.go} | 2 +- cmd/gh-slack/cmd/root.go | 65 ++++++ cmd/gh-slack/cmd/send.go | 17 ++ cmd/gh-slack/main.go | 146 +------------ go.mod | 5 +- go.sum | 11 +- 7 files changed, 298 insertions(+), 147 deletions(-) create mode 100644 cmd/gh-slack/cmd/read.go rename cmd/gh-slack/{main_test.go => cmd/read_test.go} (96%) create mode 100644 cmd/gh-slack/cmd/root.go create mode 100644 cmd/gh-slack/cmd/send.go diff --git a/cmd/gh-slack/cmd/read.go b/cmd/gh-slack/cmd/read.go new file mode 100644 index 0000000..d2fd759 --- /dev/null +++ b/cmd/gh-slack/cmd/read.go @@ -0,0 +1,199 @@ +package cmd + +import ( + "errors" + "fmt" + "io" + "log" + "net/url" + "os" + "regexp" + + "github.com/rneatherway/gh-slack/internal/gh" + "github.com/rneatherway/gh-slack/internal/markdown" + "github.com/rneatherway/gh-slack/internal/slackclient" + "github.com/rneatherway/gh-slack/internal/version" + "github.com/spf13/cobra" +) + +var readCmd = &cobra.Command{ + Use: "read [flags] ", + Short: "Reads a Slack channel and outputs the messages as markdown", + Long: `Reads a Slack channel and outputs the messages as markdown for GitHub issues.`, + RunE: func(cmd *cobra.Command, args []string) error { + return readSlack(args) + }, +} + +var ( + permalinkRE = regexp.MustCompile("https://([^./]+).slack.com/archives/([A-Z0-9]+)/p([0-9]+)([0-9]{6})") + nwoRE = regexp.MustCompile("^/[^/]+/[^/]+/?$") + issueRE = regexp.MustCompile("^/[^/]+/[^/]+/issues/[0-9]+/?$") +) + +type linkParts struct { + team string + channelID string + timestamp string +} + +// https://github.slack.com/archives/CP9GMKJCE/p1648028606962719 +// returns (github, CP9GMKJCE, 1648028606.962719, nil) +func parsePermalink(link string) (linkParts, error) { + result := permalinkRE.FindStringSubmatch(link) + if result == nil { + return linkParts{}, fmt.Errorf("not a permalink: %q", link) + } + + return linkParts{ + team: result[1], + channelID: result[2], + timestamp: result[3] + "." + result[4], + }, nil +} + +var opts struct { + Args struct { + Start string + } + Limit int + Verbose bool + Version bool + Details bool + Issue string +} + +func init() { + readCmd.Flags().IntVarP(&opts.Limit, "limit", "l", 20, "Number of _channel_ messages to be fetched after the starting message (all thread messages are fetched)") + // readCmd.Flags().BoolVarP(&opts.Verbose, "verbose", "v", false, "Show verbose debug information") + readCmd.Flags().BoolVar(&opts.Version, "version", false, "Output version information") + readCmd.Flags().BoolVarP(&opts.Details, "details", "d", false, "Wrap the markdown output in HTML
tags") + readCmd.Flags().StringVarP(&opts.Issue, "issue", "i", "", "The URL of a repository to post the output as a new issue, or the URL of an issue to add a comment to that issue") + readCmd.SetHelpTemplate(readCmdUsage) + readCmd.SetUsageTemplate(readCmdUsage) +} + +func readSlack(args []string) error { + if opts.Version { + fmt.Printf("gh-slack %s (%s)\n", version.Version(), version.Commit()) + return nil + } + + if len(args) == 0 { + return errors.New("the required argument was not provided") + } + opts.Args.Start = args[0] + if opts.Args.Start == "" { + return errors.New("the required argument was not provided") + } + + var repoUrl, issueUrl string + if opts.Issue != "" { + u, err := url.Parse(opts.Issue) + if err != nil { + return err + } + + if nwoRE.MatchString(u.Path) { + repoUrl = opts.Issue + } else if issueRE.MatchString(u.Path) { + issueUrl = opts.Issue + } else { + return fmt.Errorf("not a repository or issue URL: %q", opts.Issue) + } + } + + linkParts, err := parsePermalink(opts.Args.Start) + if err != nil { + return err + } + + logger := log.New(io.Discard, "", log.LstdFlags) + if verbose { + logger = log.Default() + } + + client, err := slackclient.New( + linkParts.team, + logger) + if err != nil { + return err + } + + history, err := client.History(linkParts.channelID, linkParts.timestamp, opts.Limit) + if err != nil { + return err + } + + output, err := markdown.FromMessages(client, history) + if err != nil { + return err + } + + var channelName string + if opts.Details { + channelInfo, err := client.ChannelInfo(linkParts.channelID) + if err != nil { + return err + } + + channelName = channelInfo.Name + output = markdown.WrapInDetails(channelName, opts.Args.Start, output) + } + + if repoUrl != "" { + if channelName == "" { + channelInfo, err := client.ChannelInfo(linkParts.channelID) + if err != nil { + return err + } + channelName = channelInfo.Name + } + + err := gh.NewIssue(repoUrl, channelName, output) + if err != nil { + return err + } + } else if issueUrl != "" { + err := gh.AddComment(issueUrl, output) + if err != nil { + return err + } + } else { + os.Stdout.WriteString(output) + } + + return nil +} + +const readCmdUsage string = `Usage:{{if .Runnable}} + {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} + {{.CommandPath}} [command] {{end}} + + where is a required argument which should be permalink for the first message to fetch. Following messages are then fetched from that channel (or thread if applicable).{{if gt (len .Aliases) 0}} +Aliases: + {{.NameAndAliases}}{{end}}{{if .HasExample}} + +Examples: +{{.Example}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}} + +Available Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}} + +{{.Title}}{{range $cmds}}{{if (and (eq .GroupID $group.ID) (or .IsAvailableCommand (eq .Name "help")))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if not .AllChildCommandsHaveGroup}} + +Additional Commands:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} + +Flags: +{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} + +Global Flags: +{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} + +Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} + {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} + +Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} +` diff --git a/cmd/gh-slack/main_test.go b/cmd/gh-slack/cmd/read_test.go similarity index 96% rename from cmd/gh-slack/main_test.go rename to cmd/gh-slack/cmd/read_test.go index cdbcd34..0b84f0c 100644 --- a/cmd/gh-slack/main_test.go +++ b/cmd/gh-slack/cmd/read_test.go @@ -1,4 +1,4 @@ -package main +package cmd import "testing" diff --git a/cmd/gh-slack/cmd/root.go b/cmd/gh-slack/cmd/root.go new file mode 100644 index 0000000..1574573 --- /dev/null +++ b/cmd/gh-slack/cmd/root.go @@ -0,0 +1,65 @@ +package cmd + +import ( + "os" + + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "gh-slack [command]", + Short: "Command line tool for interacting with Slack through gh cli", + Long: `A command line tool for interacting with Slack through the gh cli.`, +} + +func Execute() error { + cmd, _, err := rootCmd.Find(os.Args[1:]) + if err != nil || cmd == nil { + args := append([]string{"read"}, os.Args[1:]...) + rootCmd.SetArgs(args) + } + return rootCmd.Execute() +} + +var verbose bool = false + +func init() { + rootCmd.AddCommand(readCmd) + rootCmd.AddCommand(sendCmd) + rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Show verbose debug information") + rootCmd.SetHelpTemplate(rootCmdUsageTemplate) + rootCmd.SetUsageTemplate(rootCmdUsageTemplate) +} + +const rootCmdUsageTemplate string = `Usage:{{if .Runnable}} + {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} + {{.CommandPath}} [command]{{end}} + + If no command is specified, the default is "read". The default command also requires a permalink argument for the first message to fetch. + Use "gh-slack read --help" for more information about the default command behaviour.{{if gt (len .Aliases) 0}} +Aliases: + {{.NameAndAliases}}{{end}}{{if .HasExample}} + +Examples: +{{.Example}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}} + +Available Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}} + +{{.Title}}{{range $cmds}}{{if (and (eq .GroupID $group.ID) (or .IsAvailableCommand (eq .Name "help")))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if not .AllChildCommandsHaveGroup}} + +Additional Commands:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} + +Flags: +{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} + +Global Flags: +{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} + +Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} + {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} + +Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} +` diff --git a/cmd/gh-slack/cmd/send.go b/cmd/gh-slack/cmd/send.go new file mode 100644 index 0000000..1caa6f4 --- /dev/null +++ b/cmd/gh-slack/cmd/send.go @@ -0,0 +1,17 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var sendCmd = &cobra.Command{ + Use: "send", + Short: "Sends a message to a Slack channel", + Long: `Sends a message to a Slack channel.`, + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Println("send called") + return nil + }, +} diff --git a/cmd/gh-slack/main.go b/cmd/gh-slack/main.go index a94ae8d..4f078fc 100644 --- a/cmd/gh-slack/main.go +++ b/cmd/gh-slack/main.go @@ -1,156 +1,14 @@ package main import ( - "errors" "fmt" - "io" - "log" - "net/url" "os" - "regexp" - "github.com/rneatherway/gh-slack/internal/gh" - "github.com/rneatherway/gh-slack/internal/markdown" - "github.com/rneatherway/gh-slack/internal/slackclient" - "github.com/rneatherway/gh-slack/internal/version" - - "github.com/jessevdk/go-flags" -) - -var ( - permalinkRE = regexp.MustCompile("https://([^./]+).slack.com/archives/([A-Z0-9]+)/p([0-9]+)([0-9]{6})") - nwoRE = regexp.MustCompile("^/[^/]+/[^/]+/?$") - issueRE = regexp.MustCompile("^/[^/]+/[^/]+/issues/[0-9]+/?$") + "github.com/rneatherway/gh-slack/cmd/gh-slack/cmd" ) -type linkParts struct { - team string - channelID string - timestamp string -} - -// https://github.slack.com/archives/CP9GMKJCE/p1648028606962719 -// returns (github, CP9GMKJCE, 1648028606.962719, nil) -func parsePermalink(link string) (linkParts, error) { - result := permalinkRE.FindStringSubmatch(link) - if result == nil { - return linkParts{}, fmt.Errorf("not a permalink: %q", link) - } - - return linkParts{ - team: result[1], - channelID: result[2], - timestamp: result[3] + "." + result[4], - }, nil -} - -var opts struct { - Args struct { - Start string `description:"Required. Permalink for the first message to fetch. Following messages are then fetched from that channel (or thread if applicable)"` - } `positional-args:"yes"` - Limit int `short:"l" long:"limit" default:"20" description:"Number of _channel_ messages to be fetched after the starting message (all thread messages are fetched)"` - Verbose bool `short:"v" long:"verbose" description:"Show verbose debug information"` - Version bool `long:"version" description:"Output version information"` - Details bool `short:"d" long:"details" description:"Wrap the markdown output in HTML
tags"` - Issue string `short:"i" long:"issue" description:"The URL of a repository to post the output as a new issue, or the URL of an issue to add a comment to that issue"` -} - -func realMain() error { - _, err := flags.NewParser(&opts, flags.HelpFlag|flags.PassDoubleDash).Parse() - if err != nil { - return err - } - - if opts.Version { - fmt.Printf("gh-slack %s (%s)\n", version.Version(), version.Commit()) - return nil - } - - if opts.Args.Start == "" { - return errors.New("the required argument `Start` was not provided") - } - - var repoUrl, issueUrl string - if opts.Issue != "" { - u, err := url.Parse(opts.Issue) - if err != nil { - return err - } - - if nwoRE.MatchString(u.Path) { - repoUrl = opts.Issue - } else if issueRE.MatchString(u.Path) { - issueUrl = opts.Issue - } else { - return fmt.Errorf("not a repository or issue URL: %q", opts.Issue) - } - } - - linkParts, err := parsePermalink(opts.Args.Start) - if err != nil { - return err - } - - logger := log.New(io.Discard, "", log.LstdFlags) - if opts.Verbose { - logger = log.Default() - } - - client, err := slackclient.New( - linkParts.team, - logger) - if err != nil { - return err - } - - history, err := client.History(linkParts.channelID, linkParts.timestamp, opts.Limit) - if err != nil { - return err - } - - output, err := markdown.FromMessages(client, history) - if err != nil { - return err - } - - var channelName string - if opts.Details { - channelInfo, err := client.ChannelInfo(linkParts.channelID) - if err != nil { - return err - } - - channelName = channelInfo.Name - output = markdown.WrapInDetails(channelName, opts.Args.Start, output) - } - - if repoUrl != "" { - if channelName == "" { - channelInfo, err := client.ChannelInfo(linkParts.channelID) - if err != nil { - return err - } - channelName = channelInfo.Name - } - - err := gh.NewIssue(repoUrl, channelName, output) - if err != nil { - return err - } - } else if issueUrl != "" { - err := gh.AddComment(issueUrl, output) - if err != nil { - return err - } - } else { - os.Stdout.WriteString(output) - } - - return nil -} - func main() { - err := realMain() + err := cmd.Execute() if err != nil { fmt.Println(err) os.Exit(1) diff --git a/go.mod b/go.mod index b71baa3..90e625b 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/cli/go-gh v0.0.3 github.com/jessevdk/go-flags v1.5.0 github.com/keybase/go-keychain v0.0.0-20220506172723-c18928ccd7f2 + github.com/spf13/cobra v1.6.1 golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 modernc.org/sqlite v1.15.3 r00t2.io/gosecret v1.1.5 @@ -18,16 +19,18 @@ require ( github.com/godbus/dbus/v5 v5.0.6 // indirect github.com/google/uuid v1.3.0 // indirect github.com/henvic/httpretty v0.0.6 // indirect + github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/mattn/go-isatty v0.0.12 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect + github.com/spf13/pflag v1.0.5 // indirect golang.org/x/mod v0.3.0 // indirect golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/uint128 v1.1.1 // indirect modernc.org/cc/v3 v3.35.24 // indirect modernc.org/ccgo/v3 v3.15.17 // indirect diff --git a/go.sum b/go.sum index 8131590..62dea44 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,7 @@ github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5 github.com/cli/shurcooL-graphql v0.0.1 h1:/9J3t9O6p1B8zdBBtQighq5g7DQRItBwuwGh3SocsKM= github.com/cli/shurcooL-graphql v0.0.1/go.mod h1:U7gCSuMZP/Qy7kbqkk5PrqXEeDgtfG5K+W+u8weorps= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= @@ -21,6 +22,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/henvic/httpretty v0.0.6 h1:JdzGzKZBajBfnvlMALXXMVQWxWMF/ofTy8C3/OSUTxs= github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo= +github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= @@ -42,6 +45,11 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -90,8 +98,9 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU= lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= From c182347094e5c55877f37d871b8e2039d7702afa Mon Sep 17 00:00:00 2001 From: Taj Date: Wed, 22 Feb 2023 19:06:45 +0000 Subject: [PATCH 02/28] Hooked up the SendMessage API to command line utility --- cmd/gh-slack/cmd/read.go | 4 +- cmd/gh-slack/cmd/root.go | 4 ++ cmd/gh-slack/cmd/send.go | 82 ++++++++++++++++++++++++- internal/slackclient/client.go | 105 +++++++++++++++++++++++++++++++++ 4 files changed, 190 insertions(+), 5 deletions(-) diff --git a/cmd/gh-slack/cmd/read.go b/cmd/gh-slack/cmd/read.go index d2fd759..f0f70b9 100644 --- a/cmd/gh-slack/cmd/read.go +++ b/cmd/gh-slack/cmd/read.go @@ -23,6 +23,8 @@ var readCmd = &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { return readSlack(args) }, + Example: ` gh-slack read + gh-slack read -i `, } var ( @@ -57,7 +59,6 @@ var opts struct { Start string } Limit int - Verbose bool Version bool Details bool Issue string @@ -65,7 +66,6 @@ var opts struct { func init() { readCmd.Flags().IntVarP(&opts.Limit, "limit", "l", 20, "Number of _channel_ messages to be fetched after the starting message (all thread messages are fetched)") - // readCmd.Flags().BoolVarP(&opts.Verbose, "verbose", "v", false, "Show verbose debug information") readCmd.Flags().BoolVar(&opts.Version, "version", false, "Output version information") readCmd.Flags().BoolVarP(&opts.Details, "details", "d", false, "Wrap the markdown output in HTML
tags") readCmd.Flags().StringVarP(&opts.Issue, "issue", "i", "", "The URL of a repository to post the output as a new issue, or the URL of an issue to add a comment to that issue") diff --git a/cmd/gh-slack/cmd/root.go b/cmd/gh-slack/cmd/root.go index 1574573..ba6d5c1 100644 --- a/cmd/gh-slack/cmd/root.go +++ b/cmd/gh-slack/cmd/root.go @@ -10,6 +10,10 @@ var rootCmd = &cobra.Command{ Use: "gh-slack [command]", Short: "Command line tool for interacting with Slack through gh cli", Long: `A command line tool for interacting with Slack through the gh cli.`, + Example: ` gh-slack -i # defaults to read command + gh-slack read + gh-slack read -i + gh-slack send -m -c -t `, } func Execute() error { diff --git a/cmd/gh-slack/cmd/send.go b/cmd/gh-slack/cmd/send.go index 1caa6f4..b8dc82d 100644 --- a/cmd/gh-slack/cmd/send.go +++ b/cmd/gh-slack/cmd/send.go @@ -2,16 +2,92 @@ package cmd import ( "fmt" + "io" + "log" + "os" + "github.com/rneatherway/gh-slack/internal/slackclient" "github.com/spf13/cobra" ) var sendCmd = &cobra.Command{ - Use: "send", + Use: "send [flags]", Short: "Sends a message to a Slack channel", Long: `Sends a message to a Slack channel.`, RunE: func(cmd *cobra.Command, args []string) error { - fmt.Println("send called") - return nil + channelID, err := cmd.Flags().GetString("channel") + if err != nil { + return err + } + message, err := cmd.Flags().GetString("message") + if err != nil { + return err + } + team, err := cmd.Flags().GetString("team") + if err != nil { + return err + } + logger := log.New(io.Discard, "", log.LstdFlags) + if verbose { + logger = log.Default() + } + return sendMessage(team, channelID, message, logger) }, + Example: ` gh-slack send -t -c -m `, } + +// sendMessage sends a message to a Slack channel. +func sendMessage(team, channelID, message string, logger *log.Logger) error { + client, err := slackclient.New(team, logger) + if err != nil { + return err + } + resp, err := client.SendMessage(channelID, message) + if err != nil { + return err + } + fmt.Fprintln(os.Stdout, resp.Output(team, channelID)) + return nil +} + +func init() { + sendCmd.Flags().StringP("channel", "c", "", "Channel ID to send the message to (required)") + sendCmd.Flags().StringP("message", "m", "", "Message to send (required)") + sendCmd.Flags().StringP("team", "t", "", "Slack team name (required)") + sendCmd.MarkFlagRequired("channel") + sendCmd.MarkFlagRequired("message") + sendCmd.MarkFlagRequired("team") + sendCmd.MarkFlagsRequiredTogether("channel", "message", "team") + sendCmd.SetUsageTemplate(sendCmdUsage) + sendCmd.SetHelpTemplate(sendCmdUsage) +} + +const sendCmdUsage string = `Usage:{{if .Runnable}} + {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} + {{.CommandPath}}{{end}}{{if gt (len .Aliases) 0}} +Aliases: + {{.NameAndAliases}}{{end}}{{if .HasExample}} + +Examples: +{{.Example}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}} + +Available Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}} + +{{.Title}}{{range $cmds}}{{if (and (eq .GroupID $group.ID) (or .IsAvailableCommand (eq .Name "help")))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if not .AllChildCommandsHaveGroup}} + +Additional Commands:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} + +Flags: +{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} + +Global Flags: +{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} + +Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} + {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} + +Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} +` diff --git a/internal/slackclient/client.go b/internal/slackclient/client.go index 947405a..3ce7405 100644 --- a/internal/slackclient/client.go +++ b/internal/slackclient/client.go @@ -1,6 +1,7 @@ package slackclient import ( + "bytes" "encoding/json" "errors" "fmt" @@ -11,6 +12,7 @@ import ( "os" "path" "strconv" + "strings" "time" ) @@ -36,6 +38,28 @@ type Message struct { Type string } +type SendMessage struct { + ThreadTS string `json:"thread_ts,omitempty"` + Channel string `json:"channel"` // required + Text string `json:"text,omitempty"` + Attachments []Attachment `json:"attachments,omitempty"` +} + +type SendMessageResponse struct { + OK bool `json:"ok"` + Error string `json:"error,omitempty"` + Warning string `json:"warning,omitempty"` + TS string `json:"ts,omitempty"` + Message Message `json:"message,omitempty"` +} + +func (r *SendMessageResponse) Output(team, channelID string) string { + if !r.OK { + return fmt.Sprintf("Error: %s", r.Error) + } + return fmt.Sprintf("Message permalink https://%s.slack.com/archives/%s/p%s", team, channelID, strings.ReplaceAll(r.TS, ".", "")) +} + type HistoryResponse struct { CursorResponseMetadata Ok bool @@ -162,6 +186,64 @@ func (c *SlackClient) get(path string, params map[string]string) ([]byte, error) return body, nil } +func (c *SlackClient) post(path string, params map[string]string, msg *SendMessage) ([]byte, error) { + u, err := url.Parse(fmt.Sprintf("https://%s.slack.com/api/", c.team)) + if err != nil { + return nil, err + } + u.Path += path + q := u.Query() + for p := range params { + q.Add(p, params[p]) + } + u.RawQuery = q.Encode() + + var body []byte + messageBytes, err := json.Marshal(msg) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal message: %w", err) + } + reqBody := bytes.NewReader(messageBytes) + + for { + req, err := http.NewRequest(http.MethodPost, u.String(), reqBody) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json; charset=utf-8") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.auth.Token)) + for key := range c.auth.Cookies { + req.AddCookie(&http.Cookie{Name: key, Value: c.auth.Cookies[key]}) + } + + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + + body, err = io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode == 429 { + s, err := strconv.Atoi(resp.Header["Retry-After"][0]) + if err != nil { + return nil, err + } + d := time.Duration(s) + c.log.Printf("rate limited, waiting %ds", d) + time.Sleep(d * time.Second) + } else if resp.StatusCode >= 300 { + return nil, fmt.Errorf("status code %d, headers: %q, body: %q", resp.StatusCode, resp.Header, body) + } else { + break + } + } + + return body, nil +} + func (c *SlackClient) ChannelInfo(id string) (*Channel, error) { body, err := c.get("conversations.info", map[string]string{"channel": id}) @@ -368,3 +450,26 @@ func (c *SlackClient) UsernameForID(id string) (string, error) { return "", fmt.Errorf("no user with id %q", id) } + +func (c *SlackClient) SendMessage(channelID string, message string) (*SendMessageResponse, error) { + body, err := c.post("chat.postMessage", + map[string]string{}, &SendMessage{ + Channel: channelID, + Text: message, + }) + if err != nil { + return nil, err + } + + response := &SendMessageResponse{} + err = json.Unmarshal(body, response) + if err != nil { + return nil, err + } + + if !response.OK { + return nil, fmt.Errorf("chat.postMessage response not OK: %s", body) + } + + return response, nil +} From aca272b6bc4784ec5fd6ba7fff4cbb91b49fb5f7 Mon Sep 17 00:00:00 2001 From: Sid Shankar Date: Wed, 22 Feb 2023 21:07:34 -0500 Subject: [PATCH 03/28] Add a dependency on a websocket library In preparation of supporting `gh-slack send`, and to be able to support the Slack RTM API that works with websocket connections, this commit adds a dependency to nhooyr.io/websockets. --- go.mod | 2 ++ go.sum | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/go.mod b/go.mod index 90e625b..b4d8a39 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/henvic/httpretty v0.0.6 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/klauspost/compress v1.10.3 // indirect github.com/mattn/go-isatty v0.0.12 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect @@ -40,5 +41,6 @@ require ( modernc.org/opt v0.1.1 // indirect modernc.org/strutil v1.1.1 // indirect modernc.org/token v1.0.0 // indirect + nhooyr.io/websocket v1.8.7 // indirect r00t2.io/goutils v1.1.2 // indirect ) diff --git a/go.sum b/go.sum index 62dea44..d9c5238 100644 --- a/go.sum +++ b/go.sum @@ -12,33 +12,55 @@ github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro= github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3 h1:x95R7cp+rSeeqAMI2knLtQ0DKlaBhv2NrtrOvafPHRo= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/henvic/httpretty v0.0.6 h1:JdzGzKZBajBfnvlMALXXMVQWxWMF/ofTy8C3/OSUTxs= github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/keybase/go-keychain v0.0.0-20220506172723-c18928ccd7f2 h1:Qh9gCBYNeGqpMv7SMVGgansYlZgnMkuPOQZpoZNxSJs= github.com/keybase/go-keychain v0.0.0-20220506172723-c18928ccd7f2/go.mod h1:5p1xgRXY2da7ggc/67EZO7WlWAZ8TXftfCU6RtvWZJ0= +github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-sqlite3 v1.14.10 h1:MLn+5bFRlWMGoSRmJour3CL1w/qL96mvipqpwQW/Sfk= github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -51,8 +73,12 @@ github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUq github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -83,8 +109,10 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+R golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 h1:M8tBwCtWD/cZV9DZpFYRUgaymAYAr+aIUTWzDaM3uPs= @@ -97,6 +125,8 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= @@ -242,6 +272,8 @@ modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk= modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.3.2 h1:4GWBVMa48UDC7KQ9tnaggN/yTlXg+CdCX9bhgHPQ9AM= modernc.org/z v1.3.2/go.mod h1:PEU2oK2OEA1CfzDTd+8E908qEXhC9s0MfyKp5LZsd+k= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= r00t2.io/gosecret v1.1.5 h1:pmlsCR8VKeI9+BN7uvUqThP/BtgoSch580XOWin+qH4= r00t2.io/gosecret v1.1.5/go.mod h1:24jo8x5lOmWAJZJZ1J3FxhoT9+KOXSZ5pQWWzFdvdtM= r00t2.io/goutils v1.1.2 h1:zOOqNHQ/HpJVggV5NTXBcd7FQtBP2C/sMLkHw3YvBzU= From 71fc30a67a1c8431c2b74294f8909364165e8649 Mon Sep 17 00:00:00 2001 From: Sid Shankar Date: Wed, 22 Feb 2023 22:20:18 -0500 Subject: [PATCH 04/28] New type to capture RTM events This type captures a small subset of the fields that might appear in every event coming back on the websocket connection with the RTM API. --- internal/slackclient/client.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/slackclient/client.go b/internal/slackclient/client.go index 3ce7405..8de8373 100644 --- a/internal/slackclient/client.go +++ b/internal/slackclient/client.go @@ -53,6 +53,14 @@ type SendMessageResponse struct { Message Message `json:"message,omitempty"` } +type RTMEvent struct { + Type string `json:"type"` + Channel string `json:"channel,omitempty"` + User string `json:"user,omitempty"` + Text string `json:"text,omitempty"` + TS string `json:"ts,omitempty"` +} + func (r *SendMessageResponse) Output(team, channelID string) string { if !r.OK { return fmt.Sprintf("Error: %s", r.Error) From f61fcc8995688cf2273c078706e02018e1b84123 Mon Sep 17 00:00:00 2001 From: Sid Shankar Date: Wed, 22 Feb 2023 22:47:51 -0500 Subject: [PATCH 05/28] New type to capture response of Slack rtm.connect --- internal/slackclient/client.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/slackclient/client.go b/internal/slackclient/client.go index 8de8373..32eb627 100644 --- a/internal/slackclient/client.go +++ b/internal/slackclient/client.go @@ -53,6 +53,11 @@ type SendMessageResponse struct { Message Message `json:"message,omitempty"` } +type RTMConnectResponse struct { + Ok bool `json:"ok"` + URL string `json:"url"` +} + type RTMEvent struct { Type string `json:"type"` Channel string `json:"channel,omitempty"` From ac325fc34cabf79c04ac82364e9df09b4c4927a1 Mon Sep 17 00:00:00 2001 From: Sid Shankar Date: Wed, 22 Feb 2023 22:21:57 -0500 Subject: [PATCH 06/28] Setup SlackClient to have a websocket connection --- internal/slackclient/client.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/slackclient/client.go b/internal/slackclient/client.go index 32eb627..91c2c80 100644 --- a/internal/slackclient/client.go +++ b/internal/slackclient/client.go @@ -14,6 +14,8 @@ import ( "strconv" "strings" "time" + + "nhooyr.io/websocket" ) type Cursor struct { @@ -119,6 +121,7 @@ type SlackClient struct { auth *SlackAuth cache Cache log *log.Logger + ws_conn *websocket.Conn } func New(team string, log *log.Logger) (*SlackClient, error) { From 68f86fb48224cdb8091398203de0a0378e721382 Mon Sep 17 00:00:00 2001 From: Sid Shankar Date: Wed, 22 Feb 2023 22:23:45 -0500 Subject: [PATCH 07/28] New method to free up resources on deletion For now, this method only closes the websocket connection on the SlackClient --- internal/slackclient/client.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/slackclient/client.go b/internal/slackclient/client.go index 91c2c80..28335ee 100644 --- a/internal/slackclient/client.go +++ b/internal/slackclient/client.go @@ -151,6 +151,14 @@ func New(team string, log *log.Logger) (*SlackClient, error) { return c, err } +func (c *SlackClient) Close() { + // If c.ws_conn is nil, we never established a websocket connection, so there's nothing to close. + if c.ws_conn == nil { + return + } + c.ws_conn.Close(websocket.StatusNormalClosure, "") +} + func (c *SlackClient) get(path string, params map[string]string) ([]byte, error) { u, err := url.Parse(fmt.Sprintf("https://%s.slack.com/api/", c.team)) if err != nil { From c9544573e349a7dfdf3943f72ed84fa34e4f15a4 Mon Sep 17 00:00:00 2001 From: Sid Shankar Date: Wed, 22 Feb 2023 22:24:56 -0500 Subject: [PATCH 08/28] Ensure `Close` is called on SlackClient instances Whenever a SlackClient instance goes out of scope, we need to have it cleanup any resources it is holding on to. --- cmd/gh-slack/cmd/read.go | 1 + cmd/gh-slack/cmd/send.go | 1 + 2 files changed, 2 insertions(+) diff --git a/cmd/gh-slack/cmd/read.go b/cmd/gh-slack/cmd/read.go index f0f70b9..32a7f40 100644 --- a/cmd/gh-slack/cmd/read.go +++ b/cmd/gh-slack/cmd/read.go @@ -116,6 +116,7 @@ func readSlack(args []string) error { client, err := slackclient.New( linkParts.team, logger) + defer client.Close() if err != nil { return err } diff --git a/cmd/gh-slack/cmd/send.go b/cmd/gh-slack/cmd/send.go index b8dc82d..c965d8f 100644 --- a/cmd/gh-slack/cmd/send.go +++ b/cmd/gh-slack/cmd/send.go @@ -39,6 +39,7 @@ var sendCmd = &cobra.Command{ // sendMessage sends a message to a Slack channel. func sendMessage(team, channelID, message string, logger *log.Logger) error { client, err := slackclient.New(team, logger) + defer client.Close() if err != nil { return err } From 09c9109089c7f8a311af058ee96b2521e2dbbe37 Mon Sep 17 00:00:00 2001 From: Sid Shankar Date: Wed, 22 Feb 2023 22:29:53 -0500 Subject: [PATCH 09/28] Connect to the RTM API on instantiation ... and hold on to an instance of the websocket connection in the SlackClient instance. --- internal/slackclient/client.go | 37 ++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/internal/slackclient/client.go b/internal/slackclient/client.go index 28335ee..556ce4d 100644 --- a/internal/slackclient/client.go +++ b/internal/slackclient/client.go @@ -2,6 +2,7 @@ package slackclient import ( "bytes" + "context" "encoding/json" "errors" "fmt" @@ -16,6 +17,7 @@ import ( "time" "nhooyr.io/websocket" + "nhooyr.io/websocket/wsjson" ) type Cursor struct { @@ -148,6 +150,41 @@ func New(team string, log *log.Logger) (*SlackClient, error) { } err = c.loadCache() + if err != nil { + return nil, err + } + response, err := c.get("rtm.connect", + map[string]string{}) + if err != nil { + // The call to rtm.connect failed, so we can't establish a websocket connection. + // TODO: If we're attempting to execute a Send subcommand, throw an error and exit + // since we won't be able to receive responses to messages we send. + return c, err + } + connect_response := &RTMConnectResponse{} + err = json.Unmarshal(response, connect_response) + if err != nil { + // We were unable to unmarshal the response from rtm.connect, so we can't establish a websocket connection. + // TODO: If we're attempting to execute a Send subcommand, throw an error and exit + // since we won't be able to receive responses to messages we send. + return c, err + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + socket_connection, _, err := websocket.Dial(ctx, connect_response.URL, &websocket.DialOptions{}) + if err != nil { + // We were unable to establish a websocket connection. + // TODO: If we're attempting to execute a Send subcommand, throw an error and exit + // since we won't be able to receive responses to messages we send. + return c, err + } + c.ws_conn = socket_connection + // TODO: We should consider saving connect_response.URL to the cache: + // 1. rtm.connect is a Tier 1 Slack API, which means we're allowed about 1 call per minute. Short bursts are tolerated, but discouraged. + // 2. If we save connect_response.URL to the cache, we can avoid calling rtm.connect on every invocation of gh-slack. + // We'll then need to add additional logic here to "Dial" the cached wss URL, and if it fails, only then call rtm.connect. + // If we do this, we'll also need to remove calls to "c.ws_conn.Close" _unless_ there is an error. This way we keep the connection alive. return c, err } From 72c9296ca59f20317c26bdd96b5cf82a58aa59c0 Mon Sep 17 00:00:00 2001 From: Sid Shankar Date: Wed, 22 Feb 2023 22:49:22 -0500 Subject: [PATCH 10/28] Adds stub implementation to listen for messages Adds stub implementation to listen for events coming in on the websocket connection --- internal/slackclient/client.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/internal/slackclient/client.go b/internal/slackclient/client.go index 556ce4d..1cd56ba 100644 --- a/internal/slackclient/client.go +++ b/internal/slackclient/client.go @@ -534,3 +534,23 @@ func (c *SlackClient) SendMessage(channelID string, message string) (*SendMessag return response, nil } + +// TODO: Stub implementation of listening for messages +func (c *SlackClient) ListenForMessages() error { + fmt.Println("=== Reading from websocket connection... ===") + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + for i := 0; i < 5; i++ { + ws_message := &RTMEvent{} + err := wsjson.Read(ctx, c.ws_conn, ws_message) + if err != nil { + c.ws_conn.Close(websocket.StatusUnsupportedData, "") + return err + } + fmt.Println("=== Received ===") + fmt.Println(ws_message) + } + fmt.Println("=== Done Reading ===") + return nil +} From bd00ae3b0daf2769e8104f4d0d79ef8fe3026607 Mon Sep 17 00:00:00 2001 From: Taj Date: Thu, 23 Feb 2023 13:18:01 +0000 Subject: [PATCH 11/28] Updated dependencies to use cli/go-gh package --- go.mod | 30 +++++++++++++----- go.sum | 99 +++++++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 103 insertions(+), 26 deletions(-) diff --git a/go.mod b/go.mod index b4d8a39..6c277cf 100644 --- a/go.mod +++ b/go.mod @@ -4,31 +4,48 @@ go 1.18 require ( github.com/billgraziano/dpapi v0.4.0 - github.com/cli/go-gh v0.0.3 - github.com/jessevdk/go-flags v1.5.0 + github.com/cli/go-gh v1.2.0 github.com/keybase/go-keychain v0.0.0-20220506172723-c18928ccd7f2 github.com/spf13/cobra v1.6.1 golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 modernc.org/sqlite v1.15.3 + nhooyr.io/websocket v1.8.7 r00t2.io/gosecret v1.1.5 ) require ( + github.com/alecthomas/chroma v0.10.0 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/charmbracelet/glamour v0.5.1-0.20220727184942-e70ff2d969da // indirect github.com/cli/safeexec v1.0.0 // indirect - github.com/cli/shurcooL-graphql v0.0.1 // indirect + github.com/cli/shurcooL-graphql v0.0.2 // indirect + github.com/dlclark/regexp2 v1.4.0 // indirect github.com/godbus/dbus/v5 v5.0.6 // indirect github.com/google/uuid v1.3.0 // indirect + github.com/gorilla/css v1.0.0 // indirect github.com/henvic/httpretty v0.0.6 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/klauspost/compress v1.10.3 // indirect - github.com/mattn/go-isatty v0.0.12 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/microcosm-cc/bluemonday v1.0.20 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.12.0 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect + github.com/rivo/uniseg v0.2.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect + github.com/yuin/goldmark v1.4.4 // indirect + github.com/yuin/goldmark-emoji v1.0.1 // indirect golang.org/x/mod v0.3.0 // indirect - golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect - golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect + golang.org/x/net v0.7.0 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/term v0.5.0 // indirect golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -41,6 +58,5 @@ require ( modernc.org/opt v0.1.1 // indirect modernc.org/strutil v1.1.1 // indirect modernc.org/token v1.0.0 // indirect - nhooyr.io/websocket v1.8.7 // indirect r00t2.io/goutils v1.1.2 // indirect ) diff --git a/go.sum b/go.sum index d9c5238..79048db 100644 --- a/go.sum +++ b/go.sum @@ -1,33 +1,49 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= -github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= +github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/billgraziano/dpapi v0.4.0 h1:t39THI1Ld1hkkLVrhkOX6u5TUxwzRddOffq4jcwh2AE= github.com/billgraziano/dpapi v0.4.0/go.mod h1:gi1Lin0jvovT53j0EXITkY6UPb3hTfI92POaZgj9JBA= -github.com/cli/go-gh v0.0.3 h1:GcVgUa7q0SeauIRbch3VSUXVij6+c49jtAHv7WuWj5c= -github.com/cli/go-gh v0.0.3/go.mod h1:J1eNgrPJYAUy7TwPKj7GW1ibqI+WCiMndtyzrCyZIiQ= +github.com/charmbracelet/glamour v0.5.1-0.20220727184942-e70ff2d969da h1:FGz53GWQRiKQ/5xUsoCCkewSQIC7u81Scaxx2nUy3nM= +github.com/charmbracelet/glamour v0.5.1-0.20220727184942-e70ff2d969da/go.mod h1:HXz79SMFnF9arKxqeoHWxmo1BhplAH7wehlRhKQIL94= +github.com/cli/go-gh v1.2.0 h1:LjcdjdQtCWXVg3YTNEuwrHFY/amJzBXy5QjMxnWB/0Q= +github.com/cli/go-gh v1.2.0/go.mod h1:Jxk8X+TCO4Ui/GarwY9tByWm/8zp4jJktzVZNlTW5VM= github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI= github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= -github.com/cli/shurcooL-graphql v0.0.1 h1:/9J3t9O6p1B8zdBBtQighq5g7DQRItBwuwGh3SocsKM= -github.com/cli/shurcooL-graphql v0.0.1/go.mod h1:U7gCSuMZP/Qy7kbqkk5PrqXEeDgtfG5K+W+u8weorps= +github.com/cli/shurcooL-graphql v0.0.2 h1:rwP5/qQQ2fM0TzkUTwtt6E2LbIYf6R+39cUXTa04NYk= +github.com/cli/shurcooL-graphql v0.0.2/go.mod h1:tlrLmw/n5Q/+4qSvosT+9/W5zc8ZMjnJeYBxSdb4nWA= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro= github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3 h1:x95R7cp+rSeeqAMI2knLtQ0DKlaBhv2NrtrOvafPHRo= @@ -35,13 +51,16 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/henvic/httpretty v0.0.6 h1:JdzGzKZBajBfnvlMALXXMVQWxWMF/ofTy8C3/OSUTxs= github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= -github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= @@ -50,23 +69,45 @@ github.com/keybase/go-keychain v0.0.0-20220506172723-c18928ccd7f2/go.mod h1:5p1x github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.10 h1:MLn+5bFRlWMGoSRmJour3CL1w/qL96mvipqpwQW/Sfk= github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/microcosm-cc/bluemonday v1.0.19/go.mod h1:QNzV2UbLK2/53oIIwTOyLUSABMkjZ4tqiyC1g/DyqxE= +github.com/microcosm-cc/bluemonday v1.0.20 h1:flpzsq4KU3QIYAYGV/szUat7H+GPOXR0B2JU5A1Wp8Y= +github.com/microcosm-cc/bluemonday v1.0.20/go.mod h1:yfBmMi8mxvaZut3Yytv+jTXRY8mxyjJ0/kQBTElld50= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.11.0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= +github.com/muesli/termenv v0.12.0 h1:KuQRUE3PgxRFWhq4gHvZtPSLCGDqM5q/cYr1pZ39ytc= +github.com/muesli/termenv v0.12.0/go.mod h1:WCCv32tusQ/EEZ5S8oUIIrC/nIuBcxCVqlN4Xfkv+7A= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= @@ -77,9 +118,17 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:BuzhfgfWQbX0dWzYzT1zsORLnHRv3bcRcsaUk0VmXA8= +github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.4 h1:zNWRjYUW32G9KirMXYHQHVNFkXvMI7LpgNW2AgYAoIs= +github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg= +github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os= +github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -90,8 +139,10 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -101,17 +152,27 @@ golang.org/x/sys v0.0.0-20200828161417-c663848e9a16/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -124,11 +185,11 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1N golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU= From d88f5d1b0ba174f062b2f43ecabd792ef5c2429c Mon Sep 17 00:00:00 2001 From: Taj Date: Thu, 23 Feb 2023 13:18:10 +0000 Subject: [PATCH 12/28] Add ListenToMessagesFromBot method to slackclient --- internal/slackclient/client.go | 46 ++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/internal/slackclient/client.go b/internal/slackclient/client.go index 1cd56ba..d5937dc 100644 --- a/internal/slackclient/client.go +++ b/internal/slackclient/client.go @@ -16,6 +16,7 @@ import ( "strings" "time" + "github.com/cli/go-gh/pkg/markdown" "nhooyr.io/websocket" "nhooyr.io/websocket/wsjson" ) @@ -63,11 +64,20 @@ type RTMConnectResponse struct { } type RTMEvent struct { - Type string `json:"type"` - Channel string `json:"channel,omitempty"` - User string `json:"user,omitempty"` - Text string `json:"text,omitempty"` - TS string `json:"ts,omitempty"` + Type string `json:"type"` + Channel string `json:"channel,omitempty"` + User string `json:"user,omitempty"` + Text string `json:"text,omitempty"` + TS string `json:"ts,omitempty"` + BotID string `json:"bot_id,omitempty"` + BotProfile BotProfile `json:"bot_profile,omitempty"` + Subtype string `json:"subtype,omitempty"` + Attachments []Attachment `json:"attachments,omitempty"` +} + +type BotProfile struct { + ID string `json:"id"` + Name string `json:"name"` } func (r *SendMessageResponse) Output(team, channelID string) string { @@ -554,3 +564,29 @@ func (c *SlackClient) ListenForMessages() error { fmt.Println("=== Done Reading ===") return nil } + +// ListenForMessagesFromBot listens for the first message from the bot in a given channel and prints its contents +func (c *SlackClient) ListenForMessagesFromBot(channelID string, botName string) error { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + for { + ws_message := &RTMEvent{} + err := wsjson.Read(ctx, c.ws_conn, &ws_message) + if err != nil { + c.ws_conn.Close(websocket.StatusUnsupportedData, "") + return err + } + if ws_message.Channel == channelID && ws_message.Type == "message" && strings.EqualFold(ws_message.BotProfile.Name, botName) { + for _, attachment := range ws_message.Attachments { + s, err := markdown.Render(attachment.Text) + if err != nil { + return err + } + fmt.Println(s) + } + break + } + } + return nil +} From e57251e586c20cf1936c3c4126823637c6d19f73 Mon Sep 17 00:00:00 2001 From: Taj Date: Thu, 23 Feb 2023 13:18:39 +0000 Subject: [PATCH 13/28] Update send sub-command to listen to messages when a bot flag is provided --- cmd/gh-slack/cmd/send.go | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/cmd/gh-slack/cmd/send.go b/cmd/gh-slack/cmd/send.go index c965d8f..c4938bb 100644 --- a/cmd/gh-slack/cmd/send.go +++ b/cmd/gh-slack/cmd/send.go @@ -31,22 +31,39 @@ var sendCmd = &cobra.Command{ if verbose { logger = log.Default() } - return sendMessage(team, channelID, message, logger) + bot, err := cmd.Flags().GetString("bot") + if err != nil { + return err + } + err = sendMessage(team, channelID, message, bot, logger) + if err != nil { + return fmt.Errorf("failed to send message: %w", err) + } + + return sendMessage(team, channelID, message, bot, logger) }, Example: ` gh-slack send -t -c -m `, } // sendMessage sends a message to a Slack channel. -func sendMessage(team, channelID, message string, logger *log.Logger) error { +func sendMessage(team, channelID, message, bot string, logger *log.Logger) error { client, err := slackclient.New(team, logger) - defer client.Close() if err != nil { return err } + defer client.Close() resp, err := client.SendMessage(channelID, message) if err != nil { return err } + // only listen to messages when bot is specified + // TODO: maybe we should move this to a separate function (SoC) + if bot != "" { + err = client.ListenForMessagesFromBot(channelID, bot) + if err != nil { + return fmt.Errorf("failed to listen to messages: %w", err) + } + } fmt.Fprintln(os.Stdout, resp.Output(team, channelID)) return nil } @@ -58,6 +75,7 @@ func init() { sendCmd.MarkFlagRequired("channel") sendCmd.MarkFlagRequired("message") sendCmd.MarkFlagRequired("team") + sendCmd.Flags().StringP("bot", "b", "", "Name of the bot to listen to for message responses") sendCmd.MarkFlagsRequiredTogether("channel", "message", "team") sendCmd.SetUsageTemplate(sendCmdUsage) sendCmd.SetHelpTemplate(sendCmdUsage) From 9a4de812a6fd4ec06951c9e930efd79a9ed5a999 Mon Sep 17 00:00:00 2001 From: Taj Date: Thu, 23 Feb 2023 14:21:28 +0000 Subject: [PATCH 14/28] Move differred cleanup after err check --- cmd/gh-slack/cmd/read.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cmd/gh-slack/cmd/read.go b/cmd/gh-slack/cmd/read.go index 32a7f40..b36ca1a 100644 --- a/cmd/gh-slack/cmd/read.go +++ b/cmd/gh-slack/cmd/read.go @@ -113,13 +113,11 @@ func readSlack(args []string) error { logger = log.Default() } - client, err := slackclient.New( - linkParts.team, - logger) - defer client.Close() + client, err := slackclient.New(linkParts.team, logger) if err != nil { return err } + defer client.Close() history, err := client.History(linkParts.channelID, linkParts.timestamp, opts.Limit) if err != nil { From 62c5dc0c63ba360ce89b2147e3b0f493710f34c9 Mon Sep 17 00:00:00 2001 From: Robin Neatherway Date: Thu, 18 May 2023 12:25:20 +0100 Subject: [PATCH 15/28] Fix httpclient reference in new post method --- internal/slackclient/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/slackclient/client.go b/internal/slackclient/client.go index 6f00509..5120bfc 100644 --- a/internal/slackclient/client.go +++ b/internal/slackclient/client.go @@ -252,7 +252,7 @@ func (c *SlackClient) post(path string, params map[string]string, msg *SendMessa req.AddCookie(&http.Cookie{Name: key, Value: c.auth.Cookies[key]}) } - resp, err := c.client.Do(req) + resp, err := httpclient.Client.Do(req) if err != nil { return nil, err } From 315fc1775ce6fcf9c5d16f0676b966fcb1c68645 Mon Sep 17 00:00:00 2001 From: Robin Neatherway Date: Thu, 18 May 2023 16:42:07 +0100 Subject: [PATCH 16/28] Take channel name rather than id as input --- cmd/gh-slack/cmd/send.go | 20 +++++++++------- internal/slackclient/client.go | 43 ++++++++++++++++++++++++++++++---- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/cmd/gh-slack/cmd/send.go b/cmd/gh-slack/cmd/send.go index c4938bb..0511e73 100644 --- a/cmd/gh-slack/cmd/send.go +++ b/cmd/gh-slack/cmd/send.go @@ -15,7 +15,7 @@ var sendCmd = &cobra.Command{ Short: "Sends a message to a Slack channel", Long: `Sends a message to a Slack channel.`, RunE: func(cmd *cobra.Command, args []string) error { - channelID, err := cmd.Flags().GetString("channel") + channelName, err := cmd.Flags().GetString("channel") if err != nil { return err } @@ -35,23 +35,25 @@ var sendCmd = &cobra.Command{ if err != nil { return err } - err = sendMessage(team, channelID, message, bot, logger) - if err != nil { - return fmt.Errorf("failed to send message: %w", err) - } - return sendMessage(team, channelID, message, bot, logger) + return sendMessage(team, channelName, message, bot, logger) }, - Example: ` gh-slack send -t -c -m `, + Example: ` gh-slack send -t -c -m `, } // sendMessage sends a message to a Slack channel. -func sendMessage(team, channelID, message, bot string, logger *log.Logger) error { +func sendMessage(team, channelName, message, bot string, logger *log.Logger) error { client, err := slackclient.New(team, logger) if err != nil { return err } defer client.Close() + + channelID, err := client.ChannelIDForName(channelName) + if err != nil { + return err + } + resp, err := client.SendMessage(channelID, message) if err != nil { return err @@ -69,7 +71,7 @@ func sendMessage(team, channelID, message, bot string, logger *log.Logger) error } func init() { - sendCmd.Flags().StringP("channel", "c", "", "Channel ID to send the message to (required)") + sendCmd.Flags().StringP("channel", "c", "", "Channel name to send the message to (required)") sendCmd.Flags().StringP("message", "m", "", "Message to send (required)") sendCmd.Flags().StringP("team", "t", "", "Slack team name (required)") sendCmd.MarkFlagRequired("channel") diff --git a/internal/slackclient/client.go b/internal/slackclient/client.go index 21cb858..e7093ff 100644 --- a/internal/slackclient/client.go +++ b/internal/slackclient/client.go @@ -379,7 +379,9 @@ func (c *SlackClient) conversations(params map[string]string) ([]Channel, error) body, err := c.get("conversations.list", map[string]string{ "cursor": conversations.ResponseMetadata.NextCursor, - "exclude_archived": "true"}, + "exclude_archived": "true", + "limit": "1000", + }, ) if err != nil { return nil, err @@ -532,8 +534,8 @@ func (c *SlackClient) getChannelID(name string) (string, error) { } func (c *SlackClient) UsernameForID(id string) (string, error) { - if id, ok := c.cache.Users[id]; ok { - return id, nil + if name, ok := c.cache.Users[id]; ok { + return name, nil } ur, err := c.users(nil) @@ -551,8 +553,8 @@ func (c *SlackClient) UsernameForID(id string) (string, error) { return "", err } - if id, ok := c.cache.Users[id]; ok { - return id, nil + if name, ok := c.cache.Users[id]; ok { + return name, nil } body, err := c.get("users.info", map[string]string{"user": id}) @@ -579,6 +581,37 @@ func (c *SlackClient) UsernameForID(id string) (string, error) { return user.User.Name, nil } +func (c *SlackClient) ChannelIDForName(name string) (string, error) { + if id, ok := c.cache.Channels[name]; ok { + return id, nil + } + + channels, err := c.conversations(nil) + if err != nil { + return "", err + } + + c.cache.Channels = make(map[string]string) + for _, ch := range channels { + if !ch.Is_Channel { + fmt.Fprintf(os.Stderr, "Skipping non-channel %q\n", ch.Name) + continue + } + c.cache.Channels[ch.Name] = ch.ID + } + + err = c.saveCache() + if err != nil { + return "", err + } + + if id, ok := c.cache.Users[name]; ok { + return id, nil + } + + return "", fmt.Errorf("could not find any channel with name %q", name) +} + func (c *SlackClient) GetLocation() *time.Location { return c.tz } From 69dd7f1bd12fb4bf52c86727b1fb4f65f5b8dc44 Mon Sep 17 00:00:00 2001 From: Robin Neatherway Date: Thu, 18 May 2023 17:06:13 +0100 Subject: [PATCH 17/28] Allow specifying default for channel, bot and team These should go in gh's config.yml file under `extensions.slack`. --- cmd/gh-slack/cmd/send.go | 54 ++++++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/cmd/gh-slack/cmd/send.go b/cmd/gh-slack/cmd/send.go index 0511e73..4162628 100644 --- a/cmd/gh-slack/cmd/send.go +++ b/cmd/gh-slack/cmd/send.go @@ -6,39 +6,74 @@ import ( "log" "os" + "github.com/cli/go-gh/pkg/config" "github.com/rneatherway/gh-slack/internal/slackclient" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) +func getFlagOrElseConfig(cfg *config.Config, flags *pflag.FlagSet, key string) (string, error) { + value, err := flags.GetString(key) + if err != nil { + return "", err + } + + if value != "" { + return value, nil + + } + return cfg.Get([]string{"extensions", "slack", key}) +} + var sendCmd = &cobra.Command{ Use: "send [flags]", Short: "Sends a message to a Slack channel", Long: `Sends a message to a Slack channel.`, RunE: func(cmd *cobra.Command, args []string) error { - channelName, err := cmd.Flags().GetString("channel") + cfg, err := config.Read() if err != nil { return err } + + channelName, err := getFlagOrElseConfig(cfg, cmd.Flags(), "channel") + if err != nil { + return err + } + message, err := cmd.Flags().GetString("message") if err != nil { return err } - team, err := cmd.Flags().GetString("team") + + team, err := getFlagOrElseConfig(cfg, cmd.Flags(), "team") if err != nil { return err } - logger := log.New(io.Discard, "", log.LstdFlags) - if verbose { - logger = log.Default() + + wait, err := cmd.Flags().GetBool("wait") + if err != nil { + return err } + bot, err := cmd.Flags().GetString("bot") if err != nil { return err } + if wait && bot == "" { + bot, err = cfg.Get([]string{"extensions", "slack", "bot"}) + if err != nil { + return err + } + } + + logger := log.New(io.Discard, "", log.LstdFlags) + if verbose { + logger = log.Default() + } return sendMessage(team, channelName, message, bot, logger) }, - Example: ` gh-slack send -t -c -m `, + Example: ` gh-slack send -t -c -m [-b ]]`, } // sendMessage sends a message to a Slack channel. @@ -74,11 +109,10 @@ func init() { sendCmd.Flags().StringP("channel", "c", "", "Channel name to send the message to (required)") sendCmd.Flags().StringP("message", "m", "", "Message to send (required)") sendCmd.Flags().StringP("team", "t", "", "Slack team name (required)") - sendCmd.MarkFlagRequired("channel") sendCmd.MarkFlagRequired("message") - sendCmd.MarkFlagRequired("team") - sendCmd.Flags().StringP("bot", "b", "", "Name of the bot to listen to for message responses") - sendCmd.MarkFlagsRequiredTogether("channel", "message", "team") + sendCmd.Flags().StringP("bot", "b", "", "Name of the bot to listen to for message responses (implies --listen))") + sendCmd.Flags().BoolP("wait", "w", false, "Listen for message responses") + sendCmd.MarkFlagsRequiredTogether("message") sendCmd.SetUsageTemplate(sendCmdUsage) sendCmd.SetHelpTemplate(sendCmdUsage) } From d64ccedc6d4d2639bb7372a782e68dac66b15cdb Mon Sep 17 00:00:00 2001 From: Robin Neatherway Date: Fri, 19 May 2023 10:42:52 +0100 Subject: [PATCH 18/28] go mod tidy --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 178f0b2..aacbf14 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/cli/go-gh v1.2.1 github.com/keybase/go-keychain v0.0.0-20220506172723-c18928ccd7f2 github.com/spf13/cobra v1.6.1 + github.com/spf13/pflag v1.0.5 golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 modernc.org/sqlite v1.15.3 nhooyr.io/websocket v1.8.7 @@ -38,7 +39,6 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect github.com/rivo/uniseg v0.2.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect github.com/yuin/goldmark v1.4.4 // indirect github.com/yuin/goldmark-emoji v1.0.1 // indirect From a5c86e62f30adcf43436369c71e5e53d593cd3cd Mon Sep 17 00:00:00 2001 From: Robin Neatherway Date: Fri, 19 May 2023 10:43:43 +0100 Subject: [PATCH 19/28] Use camelcase --- internal/slackclient/client.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/internal/slackclient/client.go b/internal/slackclient/client.go index e7093ff..d41d735 100644 --- a/internal/slackclient/client.go +++ b/internal/slackclient/client.go @@ -140,7 +140,7 @@ type SlackClient struct { cache Cache log *log.Logger tz *time.Location - ws_conn *websocket.Conn + wsConn *websocket.Conn } func New(team string, log *log.Logger) (*SlackClient, error) { @@ -190,14 +190,14 @@ func New(team string, log *log.Logger) (*SlackClient, error) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - socket_connection, _, err := websocket.Dial(ctx, connect_response.URL, &websocket.DialOptions{}) + socketConnection, _, err := websocket.Dial(ctx, connect_response.URL, &websocket.DialOptions{}) if err != nil { // We were unable to establish a websocket connection. // TODO: If we're attempting to execute a Send subcommand, throw an error and exit // since we won't be able to receive responses to messages we send. return c, err } - c.ws_conn = socket_connection + c.wsConn = socketConnection // TODO: We should consider saving connect_response.URL to the cache: // 1. rtm.connect is a Tier 1 Slack API, which means we're allowed about 1 call per minute. Short bursts are tolerated, but discouraged. // 2. If we save connect_response.URL to the cache, we can avoid calling rtm.connect on every invocation of gh-slack. @@ -236,10 +236,10 @@ func (c *SlackClient) UsernameForMessage(message Message) (string, error) { func (c *SlackClient) Close() { // If c.ws_conn is nil, we never established a websocket connection, so there's nothing to close. - if c.ws_conn == nil { + if c.wsConn == nil { return } - c.ws_conn.Close(websocket.StatusNormalClosure, "") + c.wsConn.Close(websocket.StatusNormalClosure, "") } func (c *SlackClient) get(path string, params map[string]string) ([]byte, error) { @@ -665,14 +665,15 @@ func (c *SlackClient) ListenForMessagesFromBot(channelID string, botName string) defer cancel() for { - ws_message := &RTMEvent{} - err := wsjson.Read(ctx, c.ws_conn, &ws_message) + message := &RTMEvent{} + err := wsjson.Read(ctx, c.wsConn, &message) if err != nil { - c.ws_conn.Close(websocket.StatusUnsupportedData, "") + c.wsConn.Close(websocket.StatusUnsupportedData, "") + // TODO: what about the error from Close? return err } - if ws_message.Channel == channelID && ws_message.Type == "message" && strings.EqualFold(ws_message.BotProfile.Name, botName) { - for _, attachment := range ws_message.Attachments { + if message.Channel == channelID && message.Type == "message" && strings.EqualFold(message.BotProfile.Name, botName) { + for _, attachment := range message.Attachments { s, err := markdown.Render(attachment.Text) if err != nil { return err From 4dc9a9e540841313501f3a560c90a1480359a177 Mon Sep 17 00:00:00 2001 From: Robin Neatherway Date: Fri, 19 May 2023 10:43:58 +0100 Subject: [PATCH 20/28] Remove unused methods on SlackClient --- internal/slackclient/client.go | 51 +++------------------------------- 1 file changed, 4 insertions(+), 47 deletions(-) diff --git a/internal/slackclient/client.go b/internal/slackclient/client.go index d41d735..0064c16 100644 --- a/internal/slackclient/client.go +++ b/internal/slackclient/client.go @@ -381,6 +381,10 @@ func (c *SlackClient) conversations(params map[string]string) ([]Channel, error) "cursor": conversations.ResponseMetadata.NextCursor, "exclude_archived": "true", "limit": "1000", + + // TODO: this is the default, we might want to support private + // channels and DMs in the future + "types": "public_channel", }, ) if err != nil { @@ -506,33 +510,6 @@ func (c *SlackClient) saveCache() error { return nil } -func (c *SlackClient) getChannelID(name string) (string, error) { - if id, ok := c.cache.Channels[name]; ok { - return id, nil - } - - channels, err := c.conversations(nil) - if err != nil { - return "", err - } - - c.cache.Channels = make(map[string]string) - for _, ch := range channels { - c.cache.Channels[ch.Name] = ch.ID - } - - err = c.saveCache() - if err != nil { - return "", err - } - - if id, ok := c.cache.Channels[name]; ok { - return id, nil - } - - return "", fmt.Errorf("no channel with name %q", name) -} - func (c *SlackClient) UsernameForID(id string) (string, error) { if name, ok := c.cache.Users[id]; ok { return name, nil @@ -639,26 +616,6 @@ func (c *SlackClient) SendMessage(channelID string, message string) (*SendMessag return response, nil } -// TODO: Stub implementation of listening for messages -func (c *SlackClient) ListenForMessages() error { - fmt.Println("=== Reading from websocket connection... ===") - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - - for i := 0; i < 5; i++ { - ws_message := &RTMEvent{} - err := wsjson.Read(ctx, c.ws_conn, ws_message) - if err != nil { - c.ws_conn.Close(websocket.StatusUnsupportedData, "") - return err - } - fmt.Println("=== Received ===") - fmt.Println(ws_message) - } - fmt.Println("=== Done Reading ===") - return nil -} - // ListenForMessagesFromBot listens for the first message from the bot in a given channel and prints its contents func (c *SlackClient) ListenForMessagesFromBot(channelID string, botName string) error { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) From cc1cd543932f9b456a7c943f11718fb13420010a Mon Sep 17 00:00:00 2001 From: Robin Neatherway Date: Fri, 19 May 2023 14:33:06 +0100 Subject: [PATCH 21/28] Tidy output --- cmd/gh-slack/cmd/send.go | 6 ++++-- internal/slackclient/client.go | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/cmd/gh-slack/cmd/send.go b/cmd/gh-slack/cmd/send.go index 4162628..eca7891 100644 --- a/cmd/gh-slack/cmd/send.go +++ b/cmd/gh-slack/cmd/send.go @@ -89,7 +89,9 @@ func sendMessage(team, channelName, message, bot string, logger *log.Logger) err return err } - resp, err := client.SendMessage(channelID, message) + // We get back the permalink to the message we just sent, but I don't + // currently see a use for that. + _, err = client.SendMessage(channelID, message) if err != nil { return err } @@ -101,7 +103,7 @@ func sendMessage(team, channelName, message, bot string, logger *log.Logger) err return fmt.Errorf("failed to listen to messages: %w", err) } } - fmt.Fprintln(os.Stdout, resp.Output(team, channelID)) + return nil } diff --git a/internal/slackclient/client.go b/internal/slackclient/client.go index 0064c16..23766c3 100644 --- a/internal/slackclient/client.go +++ b/internal/slackclient/client.go @@ -635,6 +635,7 @@ func (c *SlackClient) ListenForMessagesFromBot(channelID string, botName string) if err != nil { return err } + s = strings.TrimRight(s, " \t\n") fmt.Println(s) } break From d773ae2f4ae8586ddfebb932358d09aa60416016 Mon Sep 17 00:00:00 2001 From: Robin Neatherway Date: Fri, 19 May 2023 15:51:55 +0100 Subject: [PATCH 22/28] Only connect to RTM if needed --- cmd/gh-slack/cmd/read.go | 1 - cmd/gh-slack/cmd/send.go | 26 ++++--- internal/slackclient/client.go | 114 ++++++++--------------------- internal/slackclient/rtm_client.go | 59 +++++++++++++++ 4 files changed, 105 insertions(+), 95 deletions(-) create mode 100644 internal/slackclient/rtm_client.go diff --git a/cmd/gh-slack/cmd/read.go b/cmd/gh-slack/cmd/read.go index 06a354b..0170e63 100644 --- a/cmd/gh-slack/cmd/read.go +++ b/cmd/gh-slack/cmd/read.go @@ -122,7 +122,6 @@ func readSlack(args []string) error { if err != nil { return err } - defer client.Close() history, err := client.History(linkParts.channelID, linkParts.timestamp, opts.Limit) if err != nil { diff --git a/cmd/gh-slack/cmd/send.go b/cmd/gh-slack/cmd/send.go index eca7891..65445ba 100644 --- a/cmd/gh-slack/cmd/send.go +++ b/cmd/gh-slack/cmd/send.go @@ -4,7 +4,6 @@ import ( "fmt" "io" "log" - "os" "github.com/cli/go-gh/pkg/config" "github.com/rneatherway/gh-slack/internal/slackclient" @@ -40,12 +39,12 @@ var sendCmd = &cobra.Command{ return err } - message, err := cmd.Flags().GetString("message") + team, err := getFlagOrElseConfig(cfg, cmd.Flags(), "team") if err != nil { return err } - team, err := getFlagOrElseConfig(cfg, cmd.Flags(), "team") + message, err := cmd.Flags().GetString("message") if err != nil { return err } @@ -73,7 +72,7 @@ var sendCmd = &cobra.Command{ } return sendMessage(team, channelName, message, bot, logger) }, - Example: ` gh-slack send -t -c -m [-b ]]`, + Example: ` gh-slack send -t -c -m [-b | --wait]`, } // sendMessage sends a message to a Slack channel. @@ -82,7 +81,15 @@ func sendMessage(team, channelName, message, bot string, logger *log.Logger) err if err != nil { return err } - defer client.Close() + + var rtmClient *slackclient.RTMClient + if bot != "" { + rtmClient, err := client.ConnectToRTM() + if err != nil { + return err + } + defer rtmClient.Close() + } channelID, err := client.ChannelIDForName(channelName) if err != nil { @@ -95,10 +102,9 @@ func sendMessage(team, channelName, message, bot string, logger *log.Logger) err if err != nil { return err } - // only listen to messages when bot is specified - // TODO: maybe we should move this to a separate function (SoC) + if bot != "" { - err = client.ListenForMessagesFromBot(channelID, bot) + err = rtmClient.ListenForMessagesFromBot(channelID, bot) if err != nil { return fmt.Errorf("failed to listen to messages: %w", err) } @@ -112,8 +118,8 @@ func init() { sendCmd.Flags().StringP("message", "m", "", "Message to send (required)") sendCmd.Flags().StringP("team", "t", "", "Slack team name (required)") sendCmd.MarkFlagRequired("message") - sendCmd.Flags().StringP("bot", "b", "", "Name of the bot to listen to for message responses (implies --listen))") - sendCmd.Flags().BoolP("wait", "w", false, "Listen for message responses") + sendCmd.Flags().StringP("bot", "b", "", "Name of the bot to wait for a response from (implies --wait))") + sendCmd.Flags().BoolP("wait", "w", false, "Wait for message responses") sendCmd.MarkFlagsRequiredTogether("message") sendCmd.SetUsageTemplate(sendCmdUsage) sendCmd.SetHelpTemplate(sendCmdUsage) diff --git a/internal/slackclient/client.go b/internal/slackclient/client.go index 23766c3..e5a07a4 100644 --- a/internal/slackclient/client.go +++ b/internal/slackclient/client.go @@ -18,9 +18,7 @@ import ( "github.com/rneatherway/gh-slack/internal/httpclient" - "github.com/cli/go-gh/pkg/markdown" "nhooyr.io/websocket" - "nhooyr.io/websocket/wsjson" ) type Cursor struct { @@ -61,20 +59,9 @@ type SendMessageResponse struct { } type RTMConnectResponse struct { - Ok bool `json:"ok"` - URL string `json:"url"` -} - -type RTMEvent struct { - Type string `json:"type"` - Channel string `json:"channel,omitempty"` - User string `json:"user,omitempty"` - Text string `json:"text,omitempty"` - TS string `json:"ts,omitempty"` - BotID string `json:"bot_id,omitempty"` - BotProfile BotProfile `json:"bot_profile,omitempty"` - Subtype string `json:"subtype,omitempty"` - Attachments []Attachment `json:"attachments,omitempty"` + Ok bool `json:"ok"` + Error string `json:"error"` + URL string `json:"url"` } type BotProfile struct { @@ -140,7 +127,6 @@ type SlackClient struct { cache Cache log *log.Logger tz *time.Location - wsConn *websocket.Conn } func New(team string, log *log.Logger) (*SlackClient, error) { @@ -167,43 +153,7 @@ func New(team string, log *log.Logger) (*SlackClient, error) { tz: time.Now().Location(), } - err = c.loadCache() - if err != nil { - return nil, err - } - response, err := c.get("rtm.connect", - map[string]string{}) - if err != nil { - // The call to rtm.connect failed, so we can't establish a websocket connection. - // TODO: If we're attempting to execute a Send subcommand, throw an error and exit - // since we won't be able to receive responses to messages we send. - return c, err - } - connect_response := &RTMConnectResponse{} - err = json.Unmarshal(response, connect_response) - if err != nil { - // We were unable to unmarshal the response from rtm.connect, so we can't establish a websocket connection. - // TODO: If we're attempting to execute a Send subcommand, throw an error and exit - // since we won't be able to receive responses to messages we send. - return c, err - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - socketConnection, _, err := websocket.Dial(ctx, connect_response.URL, &websocket.DialOptions{}) - if err != nil { - // We were unable to establish a websocket connection. - // TODO: If we're attempting to execute a Send subcommand, throw an error and exit - // since we won't be able to receive responses to messages we send. - return c, err - } - c.wsConn = socketConnection - // TODO: We should consider saving connect_response.URL to the cache: - // 1. rtm.connect is a Tier 1 Slack API, which means we're allowed about 1 call per minute. Short bursts are tolerated, but discouraged. - // 2. If we save connect_response.URL to the cache, we can avoid calling rtm.connect on every invocation of gh-slack. - // We'll then need to add additional logic here to "Dial" the cached wss URL, and if it fails, only then call rtm.connect. - // If we do this, we'll also need to remove calls to "c.ws_conn.Close" _unless_ there is an error. This way we keep the connection alive. - return c, err + return c, c.loadCache() } // Null produces a SlackClient suitable for testing that does not try to load @@ -234,14 +184,6 @@ func (c *SlackClient) UsernameForMessage(message Message) (string, error) { return "ghost", nil } -func (c *SlackClient) Close() { - // If c.ws_conn is nil, we never established a websocket connection, so there's nothing to close. - if c.wsConn == nil { - return - } - c.wsConn.Close(websocket.StatusNormalClosure, "") -} - func (c *SlackClient) get(path string, params map[string]string) ([]byte, error) { u, err := url.Parse(fmt.Sprintf("https://%s.slack.com/api/", c.team)) if err != nil { @@ -616,30 +558,34 @@ func (c *SlackClient) SendMessage(channelID string, message string) (*SendMessag return response, nil } -// ListenForMessagesFromBot listens for the first message from the bot in a given channel and prints its contents -func (c *SlackClient) ListenForMessagesFromBot(channelID string, botName string) error { +func (c *SlackClient) ConnectToRTM() (*RTMClient, error) { + response, err := c.get("rtm.connect", nil) + if err != nil { + return nil, err + } + + // This is a Tier 1 Slack API, which are allowed to call once a minute with + // some bursts. It would be nice to cache the URL result in case we need to + // reconnect quickly (for example if gh-slack is called in a loop by some + // external program). Although the URL is valid for 30 seconds, it seems + // that it can only be used once, so that isn't possible. + connectResponse := &RTMConnectResponse{} + err = json.Unmarshal(response, connectResponse) + if err != nil { + return nil, err + } + + if !connectResponse.Ok { + return nil, fmt.Errorf("rtm.connect response not OK: %s", response) + } + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - for { - message := &RTMEvent{} - err := wsjson.Read(ctx, c.wsConn, &message) - if err != nil { - c.wsConn.Close(websocket.StatusUnsupportedData, "") - // TODO: what about the error from Close? - return err - } - if message.Channel == channelID && message.Type == "message" && strings.EqualFold(message.BotProfile.Name, botName) { - for _, attachment := range message.Attachments { - s, err := markdown.Render(attachment.Text) - if err != nil { - return err - } - s = strings.TrimRight(s, " \t\n") - fmt.Println(s) - } - break - } + socketConnection, _, err := websocket.Dial(ctx, connectResponse.URL, &websocket.DialOptions{}) + if err != nil { + return nil, err } - return nil + + return &RTMClient{conn: socketConnection}, err } diff --git a/internal/slackclient/rtm_client.go b/internal/slackclient/rtm_client.go new file mode 100644 index 0000000..531337a --- /dev/null +++ b/internal/slackclient/rtm_client.go @@ -0,0 +1,59 @@ +package slackclient + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/cli/go-gh/pkg/markdown" + "nhooyr.io/websocket" + "nhooyr.io/websocket/wsjson" +) + +type RTMClient struct { + conn *websocket.Conn +} + +type RTMEvent struct { + Type string `json:"type"` + Channel string `json:"channel,omitempty"` + User string `json:"user,omitempty"` + Text string `json:"text,omitempty"` + TS string `json:"ts,omitempty"` + BotID string `json:"bot_id,omitempty"` + BotProfile BotProfile `json:"bot_profile,omitempty"` + Subtype string `json:"subtype,omitempty"` + Attachments []Attachment `json:"attachments,omitempty"` +} + +// ListenForMessagesFromBot listens for the first message from the bot in a given channel and prints its contents +func (c *RTMClient) ListenForMessagesFromBot(channelID, botName string) error { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + for { + message := &RTMEvent{} + err := wsjson.Read(ctx, c.conn, &message) + if err != nil { + c.conn.Close(websocket.StatusUnsupportedData, "") + return err + } + if message.Channel == channelID && message.Type == "message" && strings.EqualFold(message.BotProfile.Name, botName) { + for _, attachment := range message.Attachments { + s, err := markdown.Render(attachment.Text) + if err != nil { + return err + } + s = strings.TrimRight(s, " \t\n") + fmt.Println(s) + } + break + } + } + return nil +} + +func (c *RTMClient) Close() error { + return c.conn.Close(websocket.StatusNormalClosure, "") +} From 49185ef60da17489b151eee56797da96bbe9103b Mon Sep 17 00:00:00 2001 From: Robin Neatherway Date: Fri, 19 May 2023 16:09:31 +0100 Subject: [PATCH 23/28] Improve help output --- cmd/gh-slack/cmd/read.go | 2 +- cmd/gh-slack/cmd/root.go | 17 +++++++++++++---- cmd/gh-slack/cmd/send.go | 9 +++++---- cmd/gh-slack/main.go | 2 +- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/cmd/gh-slack/cmd/read.go b/cmd/gh-slack/cmd/read.go index 0170e63..0842843 100644 --- a/cmd/gh-slack/cmd/read.go +++ b/cmd/gh-slack/cmd/read.go @@ -68,7 +68,7 @@ func init() { readCmd.Flags().IntVarP(&opts.Limit, "limit", "l", 20, "Number of _channel_ messages to be fetched after the starting message (all thread messages are fetched)") readCmd.Flags().BoolVar(&opts.Version, "version", false, "Output version information") readCmd.Flags().BoolVarP(&opts.Details, "details", "d", false, "Wrap the markdown output in HTML
tags") - readCmd.Flags().StringVarP(&opts.Issue, "issue", "i", "", "The URL of a repository to post the output as a new issue, or the URL of an issue to add a comment to that issue") + readCmd.Flags().StringVarP(&opts.Issue, "issue", "i", "", "The URL of a repository to post the output as a new issue, or the URL of an issue (or pull request) to add a comment to") readCmd.SetHelpTemplate(readCmdUsage) readCmd.SetUsageTemplate(readCmdUsage) } diff --git a/cmd/gh-slack/cmd/root.go b/cmd/gh-slack/cmd/root.go index ba6d5c1..4d5fafe 100644 --- a/cmd/gh-slack/cmd/root.go +++ b/cmd/gh-slack/cmd/root.go @@ -7,13 +7,22 @@ import ( ) var rootCmd = &cobra.Command{ - Use: "gh-slack [command]", - Short: "Command line tool for interacting with Slack through gh cli", - Long: `A command line tool for interacting with Slack through the gh cli.`, + SilenceUsage: true, + SilenceErrors: true, + Use: "gh-slack [command]", + Short: "Command line tool for interacting with Slack through gh cli", + Long: `A command line tool for interacting with Slack through the gh cli.`, Example: ` gh-slack -i # defaults to read command gh-slack read gh-slack read -i - gh-slack send -m -c -t `, + gh-slack send -m -c -t + + # Example configuration file fragment: + extensions: + slack: + team: github + channel: ops + bot: hubot`, } func Execute() error { diff --git a/cmd/gh-slack/cmd/send.go b/cmd/gh-slack/cmd/send.go index 65445ba..fac43f1 100644 --- a/cmd/gh-slack/cmd/send.go +++ b/cmd/gh-slack/cmd/send.go @@ -72,7 +72,8 @@ var sendCmd = &cobra.Command{ } return sendMessage(team, channelName, message, bot, logger) }, - Example: ` gh-slack send -t -c -m [-b | --wait]`, + Example: ` gh-slack send -t -c -m -b + gh-slack send -m -w # If bot is specified in config`, } // sendMessage sends a message to a Slack channel. @@ -114,9 +115,9 @@ func sendMessage(team, channelName, message, bot string, logger *log.Logger) err } func init() { - sendCmd.Flags().StringP("channel", "c", "", "Channel name to send the message to (required)") - sendCmd.Flags().StringP("message", "m", "", "Message to send (required)") - sendCmd.Flags().StringP("team", "t", "", "Slack team name (required)") + sendCmd.Flags().StringP("channel", "c", "", "Channel name to send the message to (required here or in config at extensions.slack.channel)") + sendCmd.Flags().StringP("message", "m", "", "Message to send (required here or in config at extensions.slack.message)") + sendCmd.Flags().StringP("team", "t", "", "Slack team name (required here or in config at extensions.slack.team)") sendCmd.MarkFlagRequired("message") sendCmd.Flags().StringP("bot", "b", "", "Name of the bot to wait for a response from (implies --wait))") sendCmd.Flags().BoolP("wait", "w", false, "Wait for message responses") diff --git a/cmd/gh-slack/main.go b/cmd/gh-slack/main.go index 4f078fc..9866f4e 100644 --- a/cmd/gh-slack/main.go +++ b/cmd/gh-slack/main.go @@ -10,7 +10,7 @@ import ( func main() { err := cmd.Execute() if err != nil { - fmt.Println(err) + fmt.Fprintf(os.Stderr, "Error: %s\n", err) os.Exit(1) } } From a646e33a612cb6084ea2deaf5bba50f637290b77 Mon Sep 17 00:00:00 2001 From: Robin Neatherway Date: Fri, 19 May 2023 16:23:22 +0100 Subject: [PATCH 24/28] Output progress of channel fetch to stderr --- internal/slackclient/client.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/internal/slackclient/client.go b/internal/slackclient/client.go index e5a07a4..ba72d5b 100644 --- a/internal/slackclient/client.go +++ b/internal/slackclient/client.go @@ -313,7 +313,9 @@ func (c *SlackClient) ChannelInfo(id string) (*Channel, error) { return &channelInfoReponse.Channel, nil } -func (c *SlackClient) conversations(params map[string]string) ([]Channel, error) { +func (c *SlackClient) conversations() ([]Channel, error) { + fmt.Fprintf(os.Stderr, "Populating channel cache (this may take a while)...") + channels := make([]Channel, 0, 1000) conversations := &ConversationsResponse{} for { @@ -342,15 +344,14 @@ func (c *SlackClient) conversations(params map[string]string) ([]Channel, error) } channels = append(channels, conversations.Channels...) - c.log.Printf("Fetched %d channels (total so far %d)", - len(conversations.Channels), - len(channels)) + fmt.Fprintf(os.Stderr, "%d...", len(channels)) if conversations.ResponseMetadata.NextCursor == "" { break } } + fmt.Fprintf(os.Stderr, "done!\n") return channels, nil } @@ -505,7 +506,7 @@ func (c *SlackClient) ChannelIDForName(name string) (string, error) { return id, nil } - channels, err := c.conversations(nil) + channels, err := c.conversations() if err != nil { return "", err } @@ -524,7 +525,7 @@ func (c *SlackClient) ChannelIDForName(name string) (string, error) { return "", err } - if id, ok := c.cache.Users[name]; ok { + if id, ok := c.cache.Channels[name]; ok { return id, nil } From 9a4aec0f40de98cdf5a5a89f60063e644c3cfde5 Mon Sep 17 00:00:00 2001 From: Robin Neatherway Date: Fri, 19 May 2023 16:41:54 +0100 Subject: [PATCH 25/28] Update README --- README.md | 59 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 83d88ce..b47ef9d 100644 --- a/README.md +++ b/README.md @@ -15,20 +15,51 @@ This project provides a means of archiving a Slack conversation or thread as mar ## Usage Usage: - gh slack [OPTIONS] [Start] - - Application Options: - -l, --limit= Number of _channel_ messages to be fetched after the starting message (all thread messages are fetched) (default: 20) - -v, --verbose Show verbose debug information - --version Output version information - -d, --details Wrap the markdown output in HTML
tags - -i, --issue= The URL of a repository to post the output as a new issue, or the URL of an issue to add a comment to that issue - - Help Options: - -h, --help Show this help message - - Arguments: - Start: Required. Permalink for the first message to fetch. Following messages are then fetched from that channel (or thread if applicable) + gh-slack [command] + + If no command is specified, the default is "read". The default command also requires a permalink argument for the first message to fetch. + Use "gh-slack read --help" for more information about the default command behaviour. + + Examples: + gh-slack -i # defaults to read command + gh-slack read + gh-slack read -i + gh-slack send -m -c -t + + # Example configuration file fragment: + extensions: + slack: + team: github + channel: ops + bot: hubot + + Available Commands: + completion Generate the autocompletion script for the specified shell + help Help about any command + read Reads a Slack channel and outputs the messages as markdown + send Sends a message to a Slack channel + + Flags: + -h, --help help for gh-slack + -v, --verbose Show verbose debug information + + Use "gh-slack [command] --help" for more information about a command. + +## Configuration + +The `send` subcommand supports storing default values for the `team`, `bot` and +`channel` required parameters in gh's own configuration file using a block like: + +```yaml +extensions: + slack: + team: foo + channel: ops + bot: robot +``` + +This is particularly useful if you want to use the `send` subcommand to interact +with a bot serving chatops in a standard operations channel. ## Limitations From 042fa4dc407ee504ace5a3d2b2336ead2f27ea0f Mon Sep 17 00:00:00 2001 From: Robin Neatherway Date: Mon, 22 May 2023 17:19:08 +0100 Subject: [PATCH 26/28] Make help clearer about config file location --- cmd/gh-slack/cmd/root.go | 16 +++++++++------- cmd/gh-slack/cmd/send.go | 11 ++++++----- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/cmd/gh-slack/cmd/root.go b/cmd/gh-slack/cmd/root.go index 4d5fafe..ba01cad 100644 --- a/cmd/gh-slack/cmd/root.go +++ b/cmd/gh-slack/cmd/root.go @@ -6,6 +6,14 @@ import ( "github.com/spf13/cobra" ) +const sendConfigExample = ` + # Example configuration (add to gh's configuration file at $HOME/.config/gh/config.yml): + extensions: + slack: + team: github + channel: ops + bot: hubot` + var rootCmd = &cobra.Command{ SilenceUsage: true, SilenceErrors: true, @@ -16,13 +24,7 @@ var rootCmd = &cobra.Command{ gh-slack read gh-slack read -i gh-slack send -m -c -t - - # Example configuration file fragment: - extensions: - slack: - team: github - channel: ops - bot: hubot`, + ` + sendConfigExample, } func Execute() error { diff --git a/cmd/gh-slack/cmd/send.go b/cmd/gh-slack/cmd/send.go index fac43f1..9ea0434 100644 --- a/cmd/gh-slack/cmd/send.go +++ b/cmd/gh-slack/cmd/send.go @@ -73,7 +73,8 @@ var sendCmd = &cobra.Command{ return sendMessage(team, channelName, message, bot, logger) }, Example: ` gh-slack send -t -c -m -b - gh-slack send -m -w # If bot is specified in config`, + gh-slack send -m -w # If bot is specified in config +` + sendConfigExample, } // sendMessage sends a message to a Slack channel. @@ -115,11 +116,11 @@ func sendMessage(team, channelName, message, bot string, logger *log.Logger) err } func init() { - sendCmd.Flags().StringP("channel", "c", "", "Channel name to send the message to (required here or in config at extensions.slack.channel)") - sendCmd.Flags().StringP("message", "m", "", "Message to send (required here or in config at extensions.slack.message)") - sendCmd.Flags().StringP("team", "t", "", "Slack team name (required here or in config at extensions.slack.team)") + sendCmd.Flags().StringP("channel", "c", "", "Channel name to send the message to (required here or in config)") + sendCmd.Flags().StringP("message", "m", "", "Message to send (required here or in config)") + sendCmd.Flags().StringP("team", "t", "", "Slack team name (required here or in config)") sendCmd.MarkFlagRequired("message") - sendCmd.Flags().StringP("bot", "b", "", "Name of the bot to wait for a response from (implies --wait))") + sendCmd.Flags().StringP("bot", "b", "", "Name of the bot to wait for a response from (implies --wait)") sendCmd.Flags().BoolP("wait", "w", false, "Wait for message responses") sendCmd.MarkFlagsRequiredTogether("message") sendCmd.SetUsageTemplate(sendCmdUsage) From 71080a688f991bad7793479339bc7518de6bf2c2 Mon Sep 17 00:00:00 2001 From: Robin Neatherway Date: Mon, 22 May 2023 17:23:18 +0100 Subject: [PATCH 27/28] Fix shadowing error --- cmd/gh-slack/cmd/send.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/gh-slack/cmd/send.go b/cmd/gh-slack/cmd/send.go index 9ea0434..d31e627 100644 --- a/cmd/gh-slack/cmd/send.go +++ b/cmd/gh-slack/cmd/send.go @@ -86,7 +86,7 @@ func sendMessage(team, channelName, message, bot string, logger *log.Logger) err var rtmClient *slackclient.RTMClient if bot != "" { - rtmClient, err := client.ConnectToRTM() + rtmClient, err = client.ConnectToRTM() if err != nil { return err } From 7cd1a70a962a22f05486f853aa2bccfbc05317d3 Mon Sep 17 00:00:00 2001 From: Robin Neatherway Date: Tue, 23 May 2023 12:11:58 +0100 Subject: [PATCH 28/28] Support bot names/ids and file previews --- README.md | 6 +-- cmd/gh-slack/cmd/root.go | 4 +- cmd/gh-slack/cmd/send.go | 2 +- internal/slackclient/client.go | 5 ++- internal/slackclient/rtm_client.go | 64 ++++++++++++++++++++++++++---- 5 files changed, 66 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index b47ef9d..4f735ba 100644 --- a/README.md +++ b/README.md @@ -26,12 +26,12 @@ This project provides a means of archiving a Slack conversation or thread as mar gh-slack read -i gh-slack send -m -c -t - # Example configuration file fragment: + # Example configuration (add to gh's configuration file at $HOME/.config/gh/config.yml): extensions: slack: team: github channel: ops - bot: hubot + bot: hubot # Can be a user id (most reliable), bot profile name or username Available Commands: completion Generate the autocompletion script for the specified shell @@ -55,7 +55,7 @@ extensions: slack: team: foo channel: ops - bot: robot + bot: robot # Can be a user id (most reliable), bot profile name or username ``` This is particularly useful if you want to use the `send` subcommand to interact diff --git a/cmd/gh-slack/cmd/root.go b/cmd/gh-slack/cmd/root.go index ba01cad..99a2cca 100644 --- a/cmd/gh-slack/cmd/root.go +++ b/cmd/gh-slack/cmd/root.go @@ -10,9 +10,9 @@ const sendConfigExample = ` # Example configuration (add to gh's configuration file at $HOME/.config/gh/config.yml): extensions: slack: - team: github + team: foo channel: ops - bot: hubot` + bot: robot # Can be a user id (most reliable), bot profile name or username` var rootCmd = &cobra.Command{ SilenceUsage: true, diff --git a/cmd/gh-slack/cmd/send.go b/cmd/gh-slack/cmd/send.go index d31e627..102e8b3 100644 --- a/cmd/gh-slack/cmd/send.go +++ b/cmd/gh-slack/cmd/send.go @@ -120,7 +120,7 @@ func init() { sendCmd.Flags().StringP("message", "m", "", "Message to send (required here or in config)") sendCmd.Flags().StringP("team", "t", "", "Slack team name (required here or in config)") sendCmd.MarkFlagRequired("message") - sendCmd.Flags().StringP("bot", "b", "", "Name of the bot to wait for a response from (implies --wait)") + sendCmd.Flags().StringP("bot", "b", "", "User id (most reliable), profile name or username to wait for a response from (implies --wait)") sendCmd.Flags().BoolP("wait", "w", false, "Wait for message responses") sendCmd.MarkFlagsRequiredTogether("message") sendCmd.SetUsageTemplate(sendCmdUsage) diff --git a/internal/slackclient/client.go b/internal/slackclient/client.go index ba72d5b..e66f09b 100644 --- a/internal/slackclient/client.go +++ b/internal/slackclient/client.go @@ -588,5 +588,8 @@ func (c *SlackClient) ConnectToRTM() (*RTMClient, error) { return nil, err } - return &RTMClient{conn: socketConnection}, err + return &RTMClient{ + conn: socketConnection, + slackClient: c, + }, err } diff --git a/internal/slackclient/rtm_client.go b/internal/slackclient/rtm_client.go index 531337a..311484a 100644 --- a/internal/slackclient/rtm_client.go +++ b/internal/slackclient/rtm_client.go @@ -3,6 +3,7 @@ package slackclient import ( "context" "fmt" + "os" "strings" "time" @@ -12,7 +13,12 @@ import ( ) type RTMClient struct { - conn *websocket.Conn + conn *websocket.Conn + slackClient *SlackClient +} + +type File struct { + Preview string `json:"preview"` } type RTMEvent struct { @@ -25,6 +31,45 @@ type RTMEvent struct { BotProfile BotProfile `json:"bot_profile,omitempty"` Subtype string `json:"subtype,omitempty"` Attachments []Attachment `json:"attachments,omitempty"` + Files []File `json:"files,omitempty"` +} + +// correctUser checks if the message is sent by the bot/user that we are waiting +// for. We accept three possible matches against the user-provided name: +// - The bot profile's name (case-insensitive) +// - The user's ID (case-sensitive) +// - The user's name (case-insensitive) +func (c *RTMClient) correctUser(message *RTMEvent, botName string) bool { + if strings.EqualFold(message.BotProfile.Name, botName) { + return true + } + + if message.User == botName { + return true + } + + // It would be nice to just convert botName to an ID and compare that, but + // the Slack API doesn't provide a way to do that if botName is not a member + // of the team (an outside collaborator). So we have to do this the hard + // way. + user, err := c.slackClient.UsernameForID(message.User) + if err != nil { + fmt.Fprintln(os.Stderr, err) + return false + } + + return strings.EqualFold(user, botName) +} + +func trimAndPrint(text string) { + s, err := markdown.Render(strings.TrimRight(text, " \t\n")) + if err != nil { + // This is a bit lazy, but the default configuration of the markdown + // renderer cannot fail. + panic(err) + } + + fmt.Println(s) } // ListenForMessagesFromBot listens for the first message from the bot in a given channel and prints its contents @@ -39,15 +84,18 @@ func (c *RTMClient) ListenForMessagesFromBot(channelID, botName string) error { c.conn.Close(websocket.StatusUnsupportedData, "") return err } - if message.Channel == channelID && message.Type == "message" && strings.EqualFold(message.BotProfile.Name, botName) { + + if message.Channel == channelID && message.Type == "message" && c.correctUser(message, botName) { + trimAndPrint(message.Text) + for _, attachment := range message.Attachments { - s, err := markdown.Render(attachment.Text) - if err != nil { - return err - } - s = strings.TrimRight(s, " \t\n") - fmt.Println(s) + trimAndPrint(attachment.Text) + } + + for _, file := range message.Files { + trimAndPrint(file.Preview) } + break } }