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.

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