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  }