You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

150 lines
3.7 KiB

  1. // Package giphy implements a Service which adds !commands for Giphy.
  2. package giphy
  3. import (
  4. "encoding/json"
  5. "fmt"
  6. "net/http"
  7. "net/url"
  8. "strconv"
  9. "strings"
  10. "github.com/matrix-org/go-neb/types"
  11. log "github.com/sirupsen/logrus"
  12. "maunium.net/go/mautrix"
  13. "maunium.net/go/mautrix/event"
  14. mevt "maunium.net/go/mautrix/event"
  15. "maunium.net/go/mautrix/id"
  16. )
  17. // ServiceType of the Giphy service.
  18. const ServiceType = "giphy"
  19. type image struct {
  20. URL string `json:"url"`
  21. // Giphy returns ints as strings..
  22. Width string `json:"width"`
  23. Height string `json:"height"`
  24. Size string `json:"size"`
  25. }
  26. type result struct {
  27. Slug string `json:"slug"`
  28. Images struct {
  29. Downsized image `json:"downsized"`
  30. Original image `json:"original"`
  31. } `json:"images"`
  32. }
  33. type giphySearch struct {
  34. Data result `json:"data"`
  35. }
  36. // Service contains the Config fields for the Giphy Service.
  37. //
  38. // Example request:
  39. // {
  40. // "api_key": "dc6zaTOxFJmzC",
  41. // "use_downsized": false
  42. // }
  43. type Service struct {
  44. types.DefaultService
  45. // The Giphy API key to use when making HTTP requests to Giphy.
  46. // The public beta API key is "dc6zaTOxFJmzC".
  47. APIKey string `json:"api_key"`
  48. // Whether to use the downsized image from Giphy.
  49. // Uses the original image when set to false.
  50. // Defaults to false.
  51. UseDownsized bool `json:"use_downsized"`
  52. }
  53. // Commands supported:
  54. // !giphy some search query without quotes
  55. // Responds with a suitable GIF into the same room as the command.
  56. func (s *Service) Commands(client *mautrix.Client) []types.Command {
  57. return []types.Command{
  58. types.Command{
  59. Path: []string{"giphy"},
  60. Command: func(roomID id.RoomID, userID id.UserID, args []string) (interface{}, error) {
  61. return s.cmdGiphy(client, roomID, userID, args)
  62. },
  63. },
  64. }
  65. }
  66. func (s *Service) cmdGiphy(client *mautrix.Client, roomID id.RoomID, userID id.UserID, args []string) (interface{}, error) {
  67. // only 1 arg which is the text to search for.
  68. query := strings.Join(args, " ")
  69. gifResult, err := s.searchGiphy(query)
  70. if err != nil {
  71. return nil, err
  72. }
  73. image := gifResult.Images.Original
  74. if s.UseDownsized {
  75. image = gifResult.Images.Downsized
  76. }
  77. if image.URL == "" {
  78. return nil, fmt.Errorf("No results")
  79. }
  80. resUpload, err := client.UploadLink(image.URL)
  81. if err != nil {
  82. return nil, err
  83. }
  84. return mevt.MessageEventContent{
  85. MsgType: event.MsgImage,
  86. Body: gifResult.Slug,
  87. URL: resUpload.ContentURI.CUString(),
  88. Info: &mevt.FileInfo{
  89. Height: asInt(image.Height),
  90. Width: asInt(image.Width),
  91. MimeType: "image/gif",
  92. Size: asInt(image.Size),
  93. },
  94. }, nil
  95. }
  96. // searchGiphy returns info about a gif
  97. func (s *Service) searchGiphy(query string) (*result, error) {
  98. log.Info("Searching giphy for ", query)
  99. u, err := url.Parse("http://api.giphy.com/v1/gifs/translate")
  100. if err != nil {
  101. return nil, err
  102. }
  103. q := u.Query()
  104. q.Set("s", query)
  105. q.Set("api_key", s.APIKey)
  106. u.RawQuery = q.Encode()
  107. res, err := http.Get(u.String())
  108. if res != nil {
  109. defer res.Body.Close()
  110. }
  111. if err != nil {
  112. return nil, err
  113. }
  114. var search giphySearch
  115. if err := json.NewDecoder(res.Body).Decode(&search); err != nil {
  116. // Giphy returns a JSON object which has { data: [] } if there are 0 results.
  117. // This fails to be deserialised by Go.
  118. return nil, fmt.Errorf("No results")
  119. }
  120. return &search.Data, nil
  121. }
  122. func asInt(strInt string) int {
  123. i64, err := strconv.ParseInt(strInt, 10, 32)
  124. if err != nil {
  125. return 0 // default to 0 since these are all just hints to the client
  126. }
  127. return int(i64)
  128. }
  129. func init() {
  130. types.RegisterService(func(serviceID string, serviceUserID id.UserID, webhookEndpointURL string) types.Service {
  131. return &Service{
  132. DefaultService: types.NewDefaultService(serviceID, serviceUserID, ServiceType),
  133. }
  134. })
  135. }