linktaco.go 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
  2. // SPDX-License-Identifier: Apache-2.0
  3. package linktaco // import "miniflux.app/v2/internal/integration/linktaco"
  4. import (
  5. "bytes"
  6. "encoding/json"
  7. "errors"
  8. "fmt"
  9. "net/http"
  10. "strings"
  11. "time"
  12. "miniflux.app/v2/internal/config"
  13. "miniflux.app/v2/internal/http/client"
  14. "miniflux.app/v2/internal/version"
  15. )
  16. const (
  17. defaultClientTimeout = 10 * time.Second
  18. defaultGraphQLURL = "https://api.linktaco.com/query"
  19. maxTags = 10
  20. maxDescriptionLength = 500
  21. )
  22. type Client struct {
  23. graphqlURL string
  24. apiToken string
  25. orgSlug string
  26. tags string
  27. visibility string
  28. }
  29. func NewClient(apiToken, orgSlug, tags, visibility string) *Client {
  30. if visibility == "" {
  31. visibility = "PUBLIC"
  32. }
  33. return &Client{
  34. graphqlURL: defaultGraphQLURL,
  35. apiToken: apiToken,
  36. orgSlug: orgSlug,
  37. tags: tags,
  38. visibility: visibility,
  39. }
  40. }
  41. func (c *Client) CreateBookmark(entryURL, entryTitle, entryContent string) error {
  42. if c.apiToken == "" || c.orgSlug == "" {
  43. return errors.New("linktaco: missing API token or organization slug")
  44. }
  45. description := entryContent
  46. if len(description) > maxDescriptionLength {
  47. description = description[:maxDescriptionLength]
  48. }
  49. // tags (limit to 10)
  50. tags := strings.FieldsFunc(c.tags, func(c rune) bool {
  51. return c == ',' || c == ' '
  52. })
  53. if len(tags) > maxTags {
  54. tags = tags[:maxTags]
  55. }
  56. // tagsStr is used in GraphQL query to pass comma separated tags
  57. tagsStr := strings.Join(tags, ",")
  58. mutation := `
  59. mutation AddLink($input: LinkInput!) {
  60. addLink(input: $input) {
  61. id
  62. url
  63. title
  64. }
  65. }
  66. `
  67. variables := map[string]any{
  68. "input": map[string]any{
  69. "url": entryURL,
  70. "title": entryTitle,
  71. "description": description,
  72. "orgSlug": c.orgSlug,
  73. "visibility": c.visibility,
  74. "unread": true,
  75. "starred": false,
  76. "archive": false,
  77. "tags": tagsStr,
  78. },
  79. }
  80. requestBody, err := json.Marshal(map[string]any{
  81. "query": mutation,
  82. "variables": variables,
  83. })
  84. if err != nil {
  85. return fmt.Errorf("linktaco: unable to encode request body: %v", err)
  86. }
  87. request, err := http.NewRequest(http.MethodPost, c.graphqlURL, bytes.NewReader(requestBody))
  88. if err != nil {
  89. return fmt.Errorf("linktaco: unable to create request: %v", err)
  90. }
  91. request.Header.Set("Content-Type", "application/json")
  92. request.Header.Set("User-Agent", "Miniflux/"+version.Version)
  93. request.Header.Set("Authorization", "Bearer "+c.apiToken)
  94. httpClient := client.NewClientWithOptions(client.Options{Timeout: defaultClientTimeout, BlockPrivateNetworks: !config.Opts.IntegrationAllowPrivateNetworks()})
  95. response, err := httpClient.Do(request)
  96. if err != nil {
  97. return fmt.Errorf("linktaco: unable to send request: %v", err)
  98. }
  99. defer response.Body.Close()
  100. if response.StatusCode >= 400 {
  101. return fmt.Errorf("linktaco: unable to create bookmark: status=%d", response.StatusCode)
  102. }
  103. var graphqlResponse struct {
  104. Data json.RawMessage `json:"data"`
  105. Errors []json.RawMessage `json:"errors"`
  106. }
  107. if err := json.NewDecoder(response.Body).Decode(&graphqlResponse); err != nil {
  108. return fmt.Errorf("linktaco: unable to decode response: %v", err)
  109. }
  110. if len(graphqlResponse.Errors) > 0 {
  111. // Try to extract error message
  112. var errorMsg string
  113. for _, errJSON := range graphqlResponse.Errors {
  114. var errObj struct {
  115. Message string `json:"message"`
  116. }
  117. if json.Unmarshal(errJSON, &errObj) == nil && errObj.Message != "" {
  118. errorMsg = errObj.Message
  119. break
  120. }
  121. }
  122. if errorMsg == "" {
  123. // Fallback. Should never be reached.
  124. errorMsg = "GraphQL error occurred (fallback message)"
  125. }
  126. return fmt.Errorf("linktaco: %s", errorMsg)
  127. }
  128. return nil
  129. }