sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/plugins/pony/pony.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 pony adds pony images to the issue or PR in response to a /pony comment 18 package pony 19 20 import ( 21 "encoding/json" 22 "errors" 23 "fmt" 24 "net/http" 25 "net/url" 26 "regexp" 27 "strings" 28 29 "github.com/sirupsen/logrus" 30 "sigs.k8s.io/prow/pkg/config" 31 "sigs.k8s.io/prow/pkg/github" 32 "sigs.k8s.io/prow/pkg/pluginhelp" 33 "sigs.k8s.io/prow/pkg/plugins" 34 ) 35 36 // Only the properties we actually use. 37 type ponyResult struct { 38 Pony ponyResultPony `json:"pony"` 39 } 40 41 type ponyResultPony struct { 42 Representations ponyRepresentations `json:"representations"` 43 } 44 45 type ponyRepresentations struct { 46 Full string `json:"full"` 47 Small string `json:"small"` 48 } 49 50 const ( 51 ponyURL = realHerd("https://theponyapi.com/api/v1/pony/random") 52 pluginName = "pony" 53 maxPonies = 5 54 ) 55 56 var ( 57 match = regexp.MustCompile(`(?mi)^/(?:pony)(?: +(.+?))?\s*$`) 58 ) 59 60 func init() { 61 plugins.RegisterGenericCommentHandler(pluginName, handleGenericComment, helpProvider) 62 } 63 64 func helpProvider(config *plugins.Configuration, _ []config.OrgRepo) (*pluginhelp.PluginHelp, error) { 65 // The Config field is omitted because this plugin is not configurable. 66 pluginHelp := &pluginhelp.PluginHelp{ 67 Description: "The pony plugin adds a pony image to an issue or PR in response to the `/pony` command.", 68 } 69 pluginHelp.AddCommand(pluginhelp.Command{ 70 Usage: "/(pony) [pony]", 71 Description: "Add a little pony image to the issue or PR. A particular pony can optionally be named for a picture of that specific pony.", 72 Featured: false, 73 WhoCanUse: "Anyone", 74 Examples: []string{"/pony", "/pony Twilight Sparkle"}, 75 }) 76 return pluginHelp, nil 77 } 78 79 var client = http.Client{} 80 81 type githubClient interface { 82 CreateComment(owner, repo string, number int, comment string) error 83 } 84 85 type herd interface { 86 readPony(string) (string, error) 87 } 88 89 type realHerd string 90 91 func formatURLs(small, full string) string { 92 return fmt.Sprintf("[](%s)", small, full) 93 } 94 95 func (h realHerd) readPony(tags string) (string, error) { 96 uri := string(h) + "?q=" + url.QueryEscape(tags) 97 resp, err := client.Get(uri) 98 if err != nil { 99 return "", fmt.Errorf("failed to make request: %w", err) 100 } 101 defer resp.Body.Close() 102 if resp.StatusCode != http.StatusOK { 103 return "", fmt.Errorf("no pony found") 104 } 105 var a ponyResult 106 if err = json.NewDecoder(resp.Body).Decode(&a); err != nil { 107 return "", fmt.Errorf("failed to decode response: %w", err) 108 } 109 110 embedded := a.Pony.Representations.Small 111 tooBig, err := github.ImageTooBig(embedded) 112 if err != nil { 113 return "", fmt.Errorf("couldn't fetch pony for size check: %w", err) 114 } 115 if tooBig { 116 return "", fmt.Errorf("the pony is too big") 117 } 118 return formatURLs(a.Pony.Representations.Small, a.Pony.Representations.Full), nil 119 } 120 121 func handleGenericComment(pc plugins.Agent, e github.GenericCommentEvent) error { 122 return handle(pc.GitHubClient, pc.Logger, &e, ponyURL) 123 } 124 125 func handle(gc githubClient, log *logrus.Entry, e *github.GenericCommentEvent, p herd) error { 126 // Only consider new comments. 127 if e.Action != github.GenericCommentActionCreated { 128 return nil 129 } 130 // Make sure they are requesting a pony and don't allow requesting more than 'maxPonies' defined. 131 mat := match.FindAllStringSubmatch(e.Body, maxPonies) 132 if mat == nil { 133 return nil 134 } 135 136 org := e.Repo.Owner.Login 137 repo := e.Repo.Name 138 number := e.Number 139 140 var respBuilder strings.Builder 141 var tagsSpecified bool 142 for _, tag := range mat { 143 for i := 0; i < 5; i++ { 144 if tag[1] != "" { 145 tagsSpecified = true 146 } 147 resp, err := p.readPony(tag[1]) 148 if err != nil { 149 log.WithError(err).Println("Failed to get a pony") 150 continue 151 } 152 respBuilder.WriteString(resp + "\n") 153 break 154 } 155 } 156 if respBuilder.Len() > 0 { 157 return gc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, e.User.Login, respBuilder.String())) 158 } 159 160 var msg string 161 if tagsSpecified { 162 msg = "Couldn't find a pony matching given tag(s)." 163 } else { 164 msg = "https://theponyapi.com appears to be down" 165 } 166 if err := gc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, e.User.Login, msg)); err != nil { 167 log.WithError(err).Error("Failed to leave comment") 168 } 169 170 return errors.New("could not find a valid pony image") 171 }