linktaco.go 3.5 KB

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