github.com/abayer/test-infra@v0.0.5/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 issues in response to a /meow comment 18 package cat 19 20 import ( 21 "encoding/xml" 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( .+)?\s*$`) 41 meow = &realClowder{ 42 url: "http://thecatapi.com/api/images/get?format=xml&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 in response to the `/meow` command.", 58 } 59 pluginHelp.AddCommand(pluginhelp.Command{ 60 Usage: "/meow", 61 Description: "Add a cat image to the issue", 62 Featured: false, 63 WhoCanUse: "Anyone", 64 Examples: []string{"/meow", "/meow caturday"}, 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) (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 var client = http.Client{} 106 107 type catResult struct { 108 Source string `xml:"data>images>image>source_url"` 109 Image string `xml:"data>images>image>url"` 110 } 111 112 func (cr catResult) Format() (string, error) { 113 if cr.Source == "" { 114 return "", errors.New("empty source_url") 115 } 116 if cr.Image == "" { 117 return "", errors.New("empty image url") 118 } 119 src, err := url.Parse(cr.Source) 120 if err != nil { 121 return "", fmt.Errorf("invalid source_url %s: %v", cr.Source, err) 122 } 123 img, err := url.Parse(cr.Image) 124 if err != nil { 125 return "", fmt.Errorf("invalid image url %s: %v", cr.Image, err) 126 } 127 128 return fmt.Sprintf("[![cat image](%s)](%s)", img, src), nil 129 } 130 131 func (r *realClowder) Url(category string) string { 132 r.lock.RLock() 133 defer r.lock.RUnlock() 134 uri := string(r.url) 135 if category != "" { 136 uri += "&category=" + url.QueryEscape(category) 137 } 138 if r.key != "" { 139 uri += "&api_key=" + url.QueryEscape(r.key) 140 } 141 return uri 142 } 143 144 func (r *realClowder) readCat(category string) (string, error) { 145 uri := r.Url(category) 146 req, err := http.NewRequest("GET", uri, nil) 147 if err != nil { 148 return "", fmt.Errorf("could not create request %s: %v", uri, err) 149 } 150 req.Header.Add("Accept", "application/json") 151 resp, err := client.Do(req) 152 if err != nil { 153 return "", fmt.Errorf("could not read cat from %s: %v", uri, err) 154 } 155 defer resp.Body.Close() 156 var a catResult 157 if err = xml.NewDecoder(resp.Body).Decode(&a); err != nil { 158 return "", err 159 } 160 // checking size, GitHub doesn't support big images 161 toobig, err := github.ImageTooBig(a.Image) 162 if err != nil { 163 return "", err 164 } else if toobig { 165 return "", errors.New("unsupported cat :( size too big: " + a.Image) 166 } 167 return a.Format() 168 } 169 170 func handleGenericComment(pc plugins.PluginClient, e github.GenericCommentEvent) error { 171 return handle( 172 pc.GitHubClient, 173 pc.Logger, 174 &e, 175 meow, 176 func() { meow.setKey(pc.PluginConfig.Cat.KeyPath, pc.Logger) }, 177 ) 178 } 179 180 func handle(gc githubClient, log *logrus.Entry, e *github.GenericCommentEvent, c clowder, setKey func()) error { 181 // Only consider new comments. 182 if e.Action != github.GenericCommentActionCreated { 183 return nil 184 } 185 // Make sure they are requesting a cat 186 mat := match.FindStringSubmatch(e.Body) 187 if mat == nil { 188 return nil 189 } 190 191 // Now that we know this is a relevant event we can set the key. 192 setKey() 193 194 category := mat[1] 195 if len(category) > 1 { 196 category = category[1:] 197 } 198 199 org := e.Repo.Owner.Login 200 repo := e.Repo.Name 201 number := e.Number 202 203 for i := 0; i < 3; i++ { 204 resp, err := c.readCat(category) 205 if err != nil { 206 log.WithError(err).Error("Failed to get cat img") 207 continue 208 } 209 return gc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, e.User.Login, resp)) 210 } 211 212 var msg string 213 if category != "" { 214 msg = "Bad category. Please see http://thecatapi.com/api/categories/list" 215 } else { 216 msg = "http://thecatapi.com appears to be down" 217 } 218 if err := gc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, e.User.Login, msg)); err != nil { 219 log.WithError(err).Error("Failed to leave comment") 220 } 221 222 return errors.New("could not find a valid cat image") 223 }