sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/slack/client.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package slack 18 19 import ( 20 "encoding/json" 21 "fmt" 22 "io" 23 "net/http" 24 "net/url" 25 "strings" 26 27 "github.com/sirupsen/logrus" 28 ) 29 30 // HostsFlag is the flag type for slack hosts while initializing slack client 31 type HostsFlag map[string]string 32 33 func (h *HostsFlag) String() string { 34 var hosts []string 35 for host, tokenPath := range *h { 36 hosts = append(hosts, host+"="+tokenPath) 37 } 38 return strings.Join(hosts, " ") 39 } 40 41 // Set populates ProjectsFlag upon flag.Parse() 42 func (h *HostsFlag) Set(value string) error { 43 if len(*h) == 0 { 44 *h = map[string]string{} 45 } 46 parts := strings.SplitN(value, "=", 2) 47 if len(parts) != 2 { 48 return fmt.Errorf("%s not in the form of host=token-path", value) 49 } 50 host, tokenPath := parts[0], parts[1] 51 if _, ok := (*h)[host]; ok { 52 return fmt.Errorf("duplicate host: %s", host) 53 } 54 (*h)[host] = tokenPath 55 return nil 56 } 57 58 // Logger provides an interface to log debug messages. 59 type Logger interface { 60 Debugf(s string, v ...interface{}) 61 } 62 63 // Client allows you to provide connection to Slack API Server 64 // It contains a token that allows to authenticate connection to post and work with channels in the domain 65 type Client struct { 66 // If logger is non-nil, log all method calls with it. 67 logger Logger 68 69 tokenGenerator func() []byte 70 fake bool 71 } 72 73 const ( 74 chatPostMessage = "https://slack.com/api/chat.postMessage" 75 76 botName = "prow" 77 botIconEmoji = ":prow:" 78 ) 79 80 // NewClient creates a slack client with an API token. 81 func NewClient(tokenGenerator func() []byte) *Client { 82 return &Client{ 83 logger: logrus.WithField("client", "slack"), 84 tokenGenerator: tokenGenerator, 85 } 86 } 87 88 // NewFakeClient returns a client that takes no actions. 89 func NewFakeClient() *Client { 90 return &Client{ 91 fake: true, 92 } 93 } 94 95 func (sl *Client) log(methodName string, args ...interface{}) { 96 if sl.logger == nil { 97 return 98 } 99 var as []string 100 for _, arg := range args { 101 as = append(as, fmt.Sprintf("%v", arg)) 102 } 103 sl.logger.Debugf("%s(%s)", methodName, strings.Join(as, ", ")) 104 } 105 106 func (sl *Client) urlValues() *url.Values { 107 uv := url.Values{} 108 uv.Add("username", botName) 109 uv.Add("icon_emoji", botIconEmoji) 110 uv.Add("token", string(sl.tokenGenerator())) 111 return &uv 112 } 113 114 func (sl *Client) postMessage(url string, uv *url.Values) error { 115 resp, err := http.PostForm(url, *uv) 116 if err != nil { 117 return err 118 } 119 defer resp.Body.Close() 120 121 body, _ := io.ReadAll(resp.Body) 122 apiResponse := struct { 123 Ok bool `json:"ok"` 124 Error string `json:"error"` 125 }{} 126 127 if err := json.Unmarshal(body, &apiResponse); err != nil { 128 return fmt.Errorf("API returned invalid JSON (%q): %w", string(body), err) 129 } 130 131 if resp.StatusCode != 200 || !apiResponse.Ok { 132 return fmt.Errorf("request failed: %s", apiResponse.Error) 133 } 134 135 return nil 136 } 137 138 // WriteMessage adds text to channel 139 func (sl *Client) WriteMessage(text, channel string) error { 140 sl.log("WriteMessage", text, channel) 141 if sl.fake { 142 return nil 143 } 144 145 var uv = sl.urlValues() 146 uv.Add("channel", channel) 147 uv.Add("text", text) 148 149 if err := sl.postMessage(chatPostMessage, uv); err != nil { 150 return fmt.Errorf("failed to post message to %s: %w", channel, err) 151 } 152 return nil 153 }