github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/plugins/cat/cat.go (about)

     1  /*
     2  Copyright 2018 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 cat adds cat images to an issue or PR in response to a /meow comment
    18  package cat
    19  
    20  import (
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"io/ioutil"
    25  	"net/http"
    26  	"net/url"
    27  	"regexp"
    28  	"strings"
    29  	"sync"
    30  	"time"
    31  
    32  	"github.com/sirupsen/logrus"
    33  
    34  	"k8s.io/test-infra/prow/github"
    35  	"k8s.io/test-infra/prow/pluginhelp"
    36  	"k8s.io/test-infra/prow/plugins"
    37  )
    38  
    39  var (
    40  	match = regexp.MustCompile(`(?mi)^/meow(vie)?(?: (.+))?\s*$`)
    41  	meow  = &realClowder{
    42  		url: "https://api.thecatapi.com/api/images/get?format=json&results_per_page=1",
    43  	}
    44  )
    45  
    46  const (
    47  	pluginName = "cat"
    48  )
    49  
    50  func init() {
    51  	plugins.RegisterGenericCommentHandler(pluginName, handleGenericComment, helpProvider)
    52  }
    53  
    54  func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
    55  	// The Config field is omitted because this plugin is not configurable.
    56  	pluginHelp := &pluginhelp.PluginHelp{
    57  		Description: "The cat plugin adds a cat image to an issue or PR in response to the `/meow` command.",
    58  	}
    59  	pluginHelp.AddCommand(pluginhelp.Command{
    60  		Usage:       "/meow(vie) [CATegory]",
    61  		Description: "Add a cat image to the issue or PR",
    62  		Featured:    false,
    63  		WhoCanUse:   "Anyone",
    64  		Examples:    []string{"/meow", "/meow caturday", "/meowvie clothes"},
    65  	})
    66  	return pluginHelp, nil
    67  }
    68  
    69  type githubClient interface {
    70  	CreateComment(owner, repo string, number int, comment string) error
    71  }
    72  
    73  type clowder interface {
    74  	readCat(string, bool) (string, error)
    75  }
    76  
    77  type realClowder struct {
    78  	url     string
    79  	lock    sync.RWMutex
    80  	update  time.Time
    81  	key     string
    82  	keyPath string
    83  }
    84  
    85  func (c *realClowder) setKey(keyPath string, log *logrus.Entry) {
    86  	c.lock.Lock()
    87  	defer c.lock.Unlock()
    88  	if !time.Now().After(c.update) {
    89  		return
    90  	}
    91  	c.update = time.Now().Add(1 * time.Minute)
    92  	if keyPath == "" {
    93  		c.key = ""
    94  		return
    95  	}
    96  	b, err := ioutil.ReadFile(keyPath)
    97  	if err == nil {
    98  		c.key = strings.TrimSpace(string(b))
    99  		return
   100  	}
   101  	log.WithError(err).Errorf("failed to read key at %s", keyPath)
   102  	c.key = ""
   103  }
   104  
   105  type catResult struct {
   106  	Source string `json:"source_url"`
   107  	Image  string `json:"url"`
   108  }
   109  
   110  func (cr catResult) Format() (string, error) {
   111  	if cr.Source == "" {
   112  		return "", errors.New("empty source_url")
   113  	}
   114  	if cr.Image == "" {
   115  		return "", errors.New("empty image url")
   116  	}
   117  	src, err := url.Parse(cr.Source)
   118  	if err != nil {
   119  		return "", fmt.Errorf("invalid source_url %s: %v", cr.Source, err)
   120  	}
   121  	img, err := url.Parse(cr.Image)
   122  	if err != nil {
   123  		return "", fmt.Errorf("invalid image url %s: %v", cr.Image, err)
   124  	}
   125  
   126  	return fmt.Sprintf("[![cat image](%s)](%s)", img, src), nil
   127  }
   128  
   129  func (c *realClowder) URL(category string, movieCat bool) string {
   130  	c.lock.RLock()
   131  	defer c.lock.RUnlock()
   132  	uri := string(c.url)
   133  	if category != "" {
   134  		uri += "&category=" + url.QueryEscape(category)
   135  	}
   136  	if c.key != "" {
   137  		uri += "&api_key=" + url.QueryEscape(c.key)
   138  	}
   139  	if movieCat {
   140  		uri += "&mime_types=gif"
   141  	}
   142  	return uri
   143  }
   144  
   145  func (c *realClowder) readCat(category string, movieCat bool) (string, error) {
   146  	uri := c.URL(category, movieCat)
   147  	resp, err := http.Get(uri)
   148  	if err != nil {
   149  		return "", fmt.Errorf("could not read cat from %s: %v", uri, err)
   150  	}
   151  	defer resp.Body.Close()
   152  	if sc := resp.StatusCode; sc > 299 || sc < 200 {
   153  		return "", fmt.Errorf("failing %d response from %s", sc, uri)
   154  	}
   155  	cats := make([]catResult, 0)
   156  	if err = json.NewDecoder(resp.Body).Decode(&cats); err != nil {
   157  		return "", err
   158  	}
   159  	if len(cats) < 1 {
   160  		return "", fmt.Errorf("no cats in response from %s", uri)
   161  	}
   162  	a := cats[0]
   163  	if a.Image == "" {
   164  		return "", fmt.Errorf("no image url in response from %s", uri)
   165  	}
   166  	// checking size, GitHub doesn't support big images
   167  	toobig, err := github.ImageTooBig(a.Image)
   168  	if err != nil {
   169  		return "", fmt.Errorf("could not validate image size %s: %v", a.Image, err)
   170  	} else if toobig {
   171  		return "", fmt.Errorf("longcat is too long: %s", a.Image)
   172  	}
   173  	return a.Format()
   174  }
   175  
   176  func handleGenericComment(pc plugins.Agent, e github.GenericCommentEvent) error {
   177  	return handle(
   178  		pc.GitHubClient,
   179  		pc.Logger,
   180  		&e,
   181  		meow,
   182  		func() { meow.setKey(pc.PluginConfig.Cat.KeyPath, pc.Logger) },
   183  	)
   184  }
   185  
   186  func handle(gc githubClient, log *logrus.Entry, e *github.GenericCommentEvent, c clowder, setKey func()) error {
   187  	// Only consider new comments.
   188  	if e.Action != github.GenericCommentActionCreated {
   189  		return nil
   190  	}
   191  	// Make sure they are requesting a cat
   192  	mat := match.FindStringSubmatch(e.Body)
   193  	if mat == nil {
   194  		return nil
   195  	}
   196  
   197  	category, movieCat, err := parseMatch(mat)
   198  	if err != nil {
   199  		return err
   200  	}
   201  
   202  	// Now that we know this is a relevant event we can set the key.
   203  	setKey()
   204  
   205  	org := e.Repo.Owner.Login
   206  	repo := e.Repo.Name
   207  	number := e.Number
   208  
   209  	for i := 0; i < 3; i++ {
   210  		resp, err := c.readCat(category, movieCat)
   211  		if err != nil {
   212  			log.WithError(err).Error("Failed to get cat img")
   213  			continue
   214  		}
   215  		return gc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, e.User.Login, resp))
   216  	}
   217  
   218  	var msg string
   219  	if category != "" {
   220  		msg = "Bad category. Please see https://api.thecatapi.com/api/categories/list"
   221  	} else {
   222  		msg = "https://thecatapi.com appears to be down"
   223  	}
   224  	if err := gc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, e.User.Login, msg)); err != nil {
   225  		log.WithError(err).Error("Failed to leave comment")
   226  	}
   227  
   228  	return errors.New("could not find a valid cat image")
   229  }
   230  
   231  func parseMatch(mat []string) (string, bool, error) {
   232  	if len(mat) != 3 {
   233  		err := fmt.Errorf("expected 3 capture groups in regexp match, but got %d", len(mat))
   234  		return "", false, err
   235  	}
   236  	category := strings.TrimSpace(mat[2])
   237  	movieCat := len(mat[1]) > 0 // "vie" suffix is present.
   238  	return category, movieCat, nil
   239  }