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.

203 lines
5.8 KiB

  1. // Package google implements a Service which adds !commands for Google custom search engine.
  2. // Initially this package just supports image search but could be expanded to provide other functionality provided by the Google custom search engine API - https://developers.google.com/custom-search/json-api/v1/overview
  3. package google
  4. import (
  5. "encoding/json"
  6. "fmt"
  7. "io/ioutil"
  8. "math"
  9. "net/http"
  10. "net/url"
  11. "strings"
  12. "github.com/matrix-org/go-neb/types"
  13. "github.com/matrix-org/gomatrix"
  14. log "github.com/sirupsen/logrus"
  15. )
  16. // ServiceType of the Google service
  17. const ServiceType = "google"
  18. var httpClient = &http.Client{}
  19. type googleSearchResults struct {
  20. SearchInformation struct {
  21. TotalResults int64 `json:"totalResults,string"`
  22. } `json:"searchInformation"`
  23. Items []googleSearchResult `json:"items"`
  24. }
  25. type googleSearchResult struct {
  26. Title string `json:"title"`
  27. HTMLTitle string `json:"htmlTitle"`
  28. Link string `json:"link"`
  29. DisplayLink string `json:"displayLink"`
  30. Snippet string `json:"snippet"`
  31. HTMLSnippet string `json:"htmlSnippet"`
  32. Mime string `json:"mime"`
  33. FileFormat string `json:"fileFormat"`
  34. Image googleImage `json:"image"`
  35. }
  36. type googleImage struct {
  37. ContextLink string `json:"contextLink"`
  38. Height float64 `json:"height"`
  39. Width float64 `json:"width"`
  40. ByteSize int64 `json:"byteSize"`
  41. ThumbnailLink string `json:"thumbnailLink"`
  42. ThumbnailHeight float64 `json:"thumbnailHeight"`
  43. ThumbnailWidth float64 `json:"thumbnailWidth"`
  44. }
  45. // Service contains the Config fields for the Google service.
  46. //
  47. // Example request:
  48. // {
  49. // "api_key": "AIzaSyA4FD39..."
  50. // "cx": "ASdsaijwdfASD..."
  51. // }
  52. type Service struct {
  53. types.DefaultService
  54. // The Google API key to use when making HTTP requests to Google.
  55. APIKey string `json:"api_key"`
  56. // The Google custom search engine ID
  57. Cx string `json:"cx"`
  58. }
  59. // Commands supported:
  60. // !google image some_search_query_without_quotes
  61. // Responds with a suitable image into the same room as the command.
  62. func (s *Service) Commands(client *gomatrix.Client) []types.Command {
  63. return []types.Command{
  64. types.Command{
  65. Path: []string{"google", "image"},
  66. Command: func(roomID, userID string, args []string) (interface{}, error) {
  67. return s.cmdGoogleImgSearch(client, roomID, userID, args)
  68. },
  69. },
  70. types.Command{
  71. Path: []string{"google", "help"},
  72. Command: func(roomID, userID string, args []string) (interface{}, error) {
  73. return usageMessage(), nil
  74. },
  75. },
  76. types.Command{
  77. Path: []string{"google"},
  78. Command: func(roomID, userID string, args []string) (interface{}, error) {
  79. return usageMessage(), nil
  80. },
  81. },
  82. }
  83. }
  84. // usageMessage returns a matrix TextMessage representation of the service usage
  85. func usageMessage() *gomatrix.TextMessage {
  86. return &gomatrix.TextMessage{"m.notice",
  87. `Usage: !google image image_search_text`}
  88. }
  89. func (s *Service) cmdGoogleImgSearch(client *gomatrix.Client, roomID, userID string, args []string) (interface{}, error) {
  90. if len(args) < 1 {
  91. return usageMessage(), nil
  92. }
  93. // Get the query text to search for.
  94. querySentence := strings.Join(args, " ")
  95. searchResult, err := s.text2imgGoogle(querySentence)
  96. if err != nil {
  97. return nil, err
  98. }
  99. var imgURL = searchResult.Link
  100. if imgURL == "" {
  101. return gomatrix.TextMessage{
  102. MsgType: "m.notice",
  103. Body: "No image found!",
  104. }, nil
  105. }
  106. // FIXME -- Sometimes upload fails with a cryptic error - "msg=Upload request failed code=400"
  107. resUpload, err := client.UploadLink(imgURL)
  108. if err != nil {
  109. return nil, fmt.Errorf("Failed to upload Google image at URL %s (content type %s) to matrix: %s", imgURL, searchResult.Mime, err.Error())
  110. }
  111. return gomatrix.ImageMessage{
  112. MsgType: "m.image",
  113. Body: querySentence,
  114. URL: resUpload.ContentURI,
  115. Info: gomatrix.ImageInfo{
  116. Height: uint(math.Floor(searchResult.Image.Height)),
  117. Width: uint(math.Floor(searchResult.Image.Width)),
  118. Mimetype: searchResult.Mime,
  119. },
  120. }, nil
  121. }
  122. // text2imgGoogle returns info about an image
  123. func (s *Service) text2imgGoogle(query string) (*googleSearchResult, error) {
  124. log.Info("Searching Google for an image of a ", query)
  125. u, err := url.Parse("https://www.googleapis.com/customsearch/v1")
  126. if err != nil {
  127. return nil, err
  128. }
  129. q := u.Query()
  130. q.Set("q", query) // String to search for
  131. q.Set("num", "1") // Just return 1 image result
  132. q.Set("start", "1") // No search result offset
  133. q.Set("imgSize", "large") // Just search for medium size images
  134. q.Set("searchType", "image") // Search for images
  135. q.Set("key", s.APIKey) // Set the API key for the request
  136. q.Set("cx", s.Cx) // Set the custom search engine ID
  137. u.RawQuery = q.Encode()
  138. // log.Info("Request URL: ", u)
  139. res, err := httpClient.Get(u.String())
  140. if res != nil {
  141. defer res.Body.Close()
  142. }
  143. if err != nil {
  144. return nil, err
  145. }
  146. if res.StatusCode > 200 {
  147. return nil, fmt.Errorf("Request error: %d, %s", res.StatusCode, response2String(res))
  148. }
  149. var searchResults googleSearchResults
  150. // log.Info(response2String(res))
  151. if err := json.NewDecoder(res.Body).Decode(&searchResults); err != nil {
  152. return nil, fmt.Errorf("ERROR - %s", err.Error())
  153. } else if len(searchResults.Items) < 1 {
  154. return nil, fmt.Errorf("No images found")
  155. }
  156. // Return only the first search result
  157. return &searchResults.Items[0], nil
  158. }
  159. // response2String returns a string representation of an HTTP response body
  160. func response2String(res *http.Response) string {
  161. bs, err := ioutil.ReadAll(res.Body)
  162. if err != nil {
  163. return "Failed to decode response body"
  164. }
  165. str := string(bs)
  166. return str
  167. }
  168. // Initialise the service
  169. func init() {
  170. types.RegisterService(func(serviceID, serviceUserID, webhookEndpointURL string) types.Service {
  171. return &Service{
  172. DefaultService: types.NewDefaultService(serviceID, serviceUserID, ServiceType),
  173. }
  174. })
  175. }