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("[![pony image](%s)](%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  }