4
0

linktaco.go 3.5 KB

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