Skip to content

Commit

Permalink
Merge pull request #55 from nobe4/api
Browse files Browse the repository at this point in the history
feat(command): add api command
  • Loading branch information
rneatherway authored Dec 7, 2023
2 parents 6390aac + 84cff6e commit 65aa847
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 56 deletions.
136 changes: 136 additions & 0 deletions cmd/gh-slack/cmd/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package cmd

import (
"fmt"
"io"
"log"
"strings"

"github.com/cli/go-gh/pkg/config"
"github.com/rneatherway/gh-slack/internal/slackclient"
"github.com/spf13/cobra"
)

var apiCmd = &cobra.Command{
Use: "api [verb] path",
Short: "Send an API call to slack",
Long: "Send an API call to slack",
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := config.Read()
if err != nil {
return err
}

team, err := getFlagOrElseConfig(cfg, cmd.Flags(), "team")
if err != nil {
return err
}

fields, err := cmd.Flags().GetStringArray("field")
if err != nil {
return err
}

mappedFields, err := mapFields(fields)
if err != nil {
return err
}

logger := log.New(io.Discard, "", log.LstdFlags)
if verbose {
logger = log.Default()
}

client, err := slackclient.New(team, logger)
if err != nil {
return err
}

var verb, path string
if len(args) == 2 {
verb = strings.ToUpper(args[0])
path = args[1]
} else if len(args) == 1 {
path = args[0]
if body == "" {
verb = "GET"
} else {
verb = "POST"
}
} else {
return fmt.Errorf("Expected 1 or 2 arguments: verb and/or path, see help")
}

response, err := client.API(verb, path, mappedFields, body)
if err != nil {
return err
}

fmt.Println(string(response))
return nil
},
Example: ` gh-slack api get conversations.list -f types=public_channel,private_channel
gh-slack api post chat.postMessage -b '{"channel":"123","blocks":[...]}`,
}

var fields []string
var body string

func init() {
apiCmd.Flags().StringArrayVarP(&fields, "field", "f", nil, "Fields to pass to the api call")
apiCmd.Flags().StringVarP(&body, "body", "b", "", "Body to send as JSON")
apiCmd.Flags().StringP("team", "t", "", "Slack team name (required here or in config)")
apiCmd.SetHelpTemplate(apiCmdUsage)
apiCmd.SetUsageTemplate(apiCmdUsage)
}

func mapFields(fields []string) (map[string]string, error) {
mappedFields := map[string]string{}

for _, field := range fields {
parts := strings.SplitN(field, "=", 2)

if len(parts) != 2 || parts[1] == "" {
return nil, fmt.Errorf("field '%s' is missing a value", field)
}

mappedFields[parts[0]] = parts[1]
}

return mappedFields, nil
}

const apiCmdUsage string = `Usage:{{if .Runnable}}
{{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
{{.CommandPath}}{{end}}{{if gt (len .Aliases) 0}}
Aliases:
{{.NameAndAliases}}{{end}}{{if .HasExample}}
Examples:
{{.Example}}
The verb is optional:
- If no body is sent, GET will be used.
- If a body is sent, POST will be used.
{{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}}
`
2 changes: 2 additions & 0 deletions cmd/gh-slack/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ var rootCmd = &cobra.Command{
gh-slack read <slack-permalink>
gh-slack read -i <issue-url> <slack-permalink>
gh-slack send -m <message> -c <channel-name> -t <team-name>
gh-slack api post chat.postMessage -b '{"channel":"123","blocks":[...]}
` + sendConfigExample,
}

Expand All @@ -41,6 +42,7 @@ var verbose bool = false
func init() {
rootCmd.AddCommand(readCmd)
rootCmd.AddCommand(sendCmd)
rootCmd.AddCommand(apiCmd)
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Show verbose debug information")
rootCmd.SetHelpTemplate(rootCmdUsageTemplate)
rootCmd.SetUsageTemplate(rootCmdUsageTemplate)
Expand Down
78 changes: 22 additions & 56 deletions internal/slackclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,25 +184,36 @@ func (c *SlackClient) UsernameForMessage(message Message) (string, error) {
return "ghost", nil
}

func (c *SlackClient) get(path string, params map[string]string) ([]byte, error) {
func (c *SlackClient) API(verb, path string, params map[string]string, body string) ([]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()
q.Add("token", c.auth.Token)
for p := range params {
q.Add(p, params[p])
}
u.RawQuery = q.Encode()

var body []byte
messageBytes := []byte(body)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal message: %w", err)
}

reqBody := bytes.NewReader(messageBytes)

resBody := []byte{}

for {
req, err := http.NewRequest("GET", u.String(), nil)
req, err := http.NewRequest(verb, u.String(), reqBody)
if err != nil {
return nil, err
}
// FIXME: this doesn't seem to break non-POST/non-data requests, but migth
// be polluting the headers.
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]})
}
Expand All @@ -212,7 +223,7 @@ func (c *SlackClient) get(path string, params map[string]string) ([]byte, error)
return nil, err
}

body, err = io.ReadAll(resp.Body)
resBody, err = io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
Expand All @@ -232,65 +243,20 @@ func (c *SlackClient) get(path string, params map[string]string) ([]byte, error)
}
}

return body, nil
return resBody, 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()
func (c *SlackClient) get(path string, params map[string]string) ([]byte, error) {
return c.API("GET", path, params, "{}")
}

var body []byte
func (c *SlackClient) post(path string, params map[string]string, msg *SendMessage) ([]byte, error) {
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 := httpclient.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
return c.API("POST", path, params, string(messageBytes))
}

func (c *SlackClient) ChannelInfo(id string) (*Channel, error) {
Expand Down

0 comments on commit 65aa847

Please sign in to comment.