github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/plugins/yuks/yuks.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 yuks 18 19 import ( 20 "bytes" 21 "encoding/json" 22 "fmt" 23 "net/http" 24 "regexp" 25 26 "github.com/sirupsen/logrus" 27 28 "sigs.k8s.io/prow/pkg/config" 29 "sigs.k8s.io/prow/pkg/github" 30 "sigs.k8s.io/prow/pkg/pluginhelp" 31 "sigs.k8s.io/prow/pkg/plugins" 32 ) 33 34 var ( 35 match = regexp.MustCompile(`(?mi)^/joke\s*$`) 36 simple = regexp.MustCompile(`^[\w?'!., ]+$`) 37 ) 38 39 const ( 40 // Previously: https://tambal.azurewebsites.net/joke/random 41 jokeURL = realJoke("https://icanhazdadjoke.com") 42 pluginName = "yuks" 43 ) 44 45 func init() { 46 plugins.RegisterGenericCommentHandler(pluginName, handleGenericComment, helpProvider) 47 } 48 49 func helpProvider(config *plugins.Configuration, _ []config.OrgRepo) (*pluginhelp.PluginHelp, error) { 50 // The Config field is omitted because this plugin is not configurable. 51 pluginHelp := &pluginhelp.PluginHelp{ 52 Description: "The yuks plugin comments with jokes in response to the `/joke` command.", 53 } 54 pluginHelp.AddCommand(pluginhelp.Command{ 55 Usage: "/joke", 56 Description: "Tells a joke.", 57 Featured: false, 58 WhoCanUse: "Anyone can use the `/joke` command.", 59 Examples: []string{"/joke"}, 60 }) 61 return pluginHelp, nil 62 } 63 64 type githubClient interface { 65 CreateComment(owner, repo string, number int, comment string) error 66 } 67 68 type joker interface { 69 readJoke() (string, error) 70 } 71 72 type realJoke string 73 74 var client = http.Client{} 75 76 type jokeResult struct { 77 Joke string `json:"joke"` 78 } 79 80 func (url realJoke) readJoke() (string, error) { 81 req, err := http.NewRequest("GET", string(url), nil) 82 if err != nil { 83 return "", fmt.Errorf("could not create request %s: %w", url, err) 84 } 85 req.Header.Add("Accept", "application/json") 86 resp, err := client.Do(req) 87 if err != nil { 88 return "", fmt.Errorf("could not read joke from %s: %w", url, err) 89 } 90 defer resp.Body.Close() 91 var a jokeResult 92 if err = json.NewDecoder(resp.Body).Decode(&a); err != nil { 93 return "", err 94 } 95 if a.Joke == "" { 96 return "", fmt.Errorf("result from %s did not contain a joke", url) 97 } 98 return a.Joke, nil 99 } 100 101 func handleGenericComment(pc plugins.Agent, e github.GenericCommentEvent) error { 102 return handle(pc.GitHubClient, pc.Logger, &e, jokeURL) 103 } 104 105 // escapeMarkdown takes a string and returns a serialized version of it such that all the symbols 106 // are treated as text instead of Markdown syntax. It escapes the symbols using numeric character 107 // references with the decimal notation. See https://www.w3.org/TR/html401/charset.html#h-5.3.1 108 func escapeMarkdown(s string) string { 109 var b bytes.Buffer 110 for _, r := range s { 111 // Check for simple characters as they are considered safe, otherwise we escape the rune. 112 c := string(r) 113 if simple.MatchString(c) { 114 b.WriteString(c) 115 } else { 116 b.WriteString(fmt.Sprintf("&#%d;", r)) 117 } 118 } 119 return b.String() 120 } 121 122 func handle(gc githubClient, log *logrus.Entry, e *github.GenericCommentEvent, j joker) error { 123 // Only consider new comments. 124 if e.Action != github.GenericCommentActionCreated { 125 return nil 126 } 127 // Make sure they are requesting a joke 128 if !match.MatchString(e.Body) { 129 return nil 130 } 131 132 org := e.Repo.Owner.Login 133 repo := e.Repo.Name 134 number := e.Number 135 136 errorBudget := 5 137 for i := 1; i <= errorBudget; i++ { 138 resp, err := j.readJoke() 139 if err != nil { 140 log.WithError(err).Infof("failed to get joke. Retrying (attempt %d/%d)", i, errorBudget) 141 continue 142 } 143 if resp == "" { 144 log.Infof("joke is empty. Retrying (attempt %d/%d)", i, errorBudget) 145 continue 146 } 147 148 sanitizedJoke := escapeMarkdown(resp) 149 log.Infof("commenting with \"%s\".", sanitizedJoke) 150 return gc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, e.User.Login, sanitizedJoke)) 151 } 152 153 return fmt.Errorf("failed to get joke after %d attempts", errorBudget) 154 }