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  }