From 5ba3c56b75d5d320d7ebb46260e2358753365c99 Mon Sep 17 00:00:00 2001 From: ekzyis Date: Sun, 7 Apr 2024 03:08:50 +0200 Subject: [PATCH] Refactor code to use Client struct * previous code was messy with all source in project root * now code is organized in pkg/ * removed types.go and moved types into their respective modules * package exports a Client struct now and all functions are methods of that struct * also supports API keys now --- README.md | 28 ++--- dupes.go | 47 ------- go.mod | 7 +- go.sum | 4 - items.go | 167 ------------------------- main.go | 81 ------------ notes.go | 34 ----- pkg/client.go | 78 ++++++++++++ pkg/items.go | 339 ++++++++++++++++++++++++++++++++++++++++++++++++++ pkg/rss.go | 70 +++++++++++ pkg/user.go | 6 + rss.go | 45 ------- session.go | 23 ---- types.go | 140 --------------------- 14 files changed, 508 insertions(+), 561 deletions(-) delete mode 100644 dupes.go delete mode 100644 items.go delete mode 100644 main.go delete mode 100644 notes.go create mode 100644 pkg/client.go create mode 100644 pkg/items.go create mode 100644 pkg/rss.go create mode 100644 pkg/user.go delete mode 100644 rss.go delete mode 100644 session.go delete mode 100644 types.go diff --git a/README.md b/README.md index a3312e7..3b0a6c3 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,19 @@ -# sn-goapi +# snappy -Package for [stacker.news](https://stacker.news) API access +
-Install via `go get github.com/ekzyis/sn-goapi` + + -Supports: +[stacker.news](https://stacker.news) API client package for Go -- [ ] Post of type ... - - [ ] ... Discussion - - [x] ... Link - - [ ] ... Bounty - - [ ] ... Poll -- [x] Reply (comments) -- [ ] Tips -- [x] Checking dupes -- [x] Checking notifications +
-`SN_AUTH_COOKIE` must be set with a valid session cookie. + +## How to use + +``` +$ go get github.com/ekzyis/snappy +``` + +`SN_API_KEY` must be set in your environment for authenticated API access. diff --git a/dupes.go b/dupes.go deleted file mode 100644 index 4f9a524..0000000 --- a/dupes.go +++ /dev/null @@ -1,47 +0,0 @@ -package sn - -import ( - "encoding/json" - "fmt" -) - -// Fetch dupes -func Dupes(url string) (*[]Dupe, error) { - body := GraphQLPayload{ - Query: ` - query Dupes($url: String!) { - dupes(url: $url) { - id - url - title - user { - name - } - createdAt - sats - ncomments - } - }`, - Variables: map[string]interface{}{ - "url": url, - }, - } - resp, err := MakeStackerNewsRequest(body) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - var respBody DupesResponse - err = json.NewDecoder(resp.Body).Decode(&respBody) - if err != nil { - err = fmt.Errorf("error decoding SN dupes: %w", err) - return nil, err - } - err = CheckForErrors(respBody.Errors) - if err != nil { - return nil, err - } - - return &respBody.Data.Dupes, nil -} diff --git a/go.mod b/go.mod index 1a1f379..a1b4a19 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,3 @@ -module github.com/ekzyis/sn-goapi +module github.com/ekzyis/snappy go 1.20 - -require ( - github.com/joho/godotenv v1.5.1 // indirect - github.com/namsral/flag v1.7.4-pre // indirect -) diff --git a/go.sum b/go.sum index 4c16f8b..e69de29 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +0,0 @@ -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/namsral/flag v1.7.4-pre h1:b2ScHhoCUkbsq0d2C15Mv+VU8bl8hAXV8arnWiOHNZs= -github.com/namsral/flag v1.7.4-pre/go.mod h1:OXldTctbM6SWH1K899kPZcf65KxJiD7MsceFUpB5yDo= diff --git a/items.go b/items.go deleted file mode 100644 index 180afc7..0000000 --- a/items.go +++ /dev/null @@ -1,167 +0,0 @@ -package sn - -import ( - "encoding/json" - "fmt" -) - -func Items(query *ItemsQuery) (*ItemsCursor, error) { - if query == nil { - query = &ItemsQuery{} - } - - if sub := query.Sub; sub != "" { - if !(sub == "bitcoin" || sub == "nostr" || sub == "tech" || sub == "meta") { - return nil, fmt.Errorf("invalid sub: %s", sub) - } - } - - body := GraphQLPayload{ - Query: ` - query items($sub: String, $sort: String, $cursor: String, $type: String, $name: String, $when: String, $by: String, $limit: Limit) { - items(sub: $sub, sort: $sort, cursor: $cursor, type: $type, name: $name, when: $when, by: $by, limit: $limit) { - cursor - items { - id - parentId - createdAt - deletedAt - title - url - user { - id - name - } - otsHash - position - sats - boost - bounty - bountyPaidTo - path - upvotes - meSats - meDontLikeSats - meBookmark - meSubscription - outlawed - freebie - ncomments - commentSats - lastCommentAt - maxBid - isJob - company - location - remote - subName - pollCost - status - uploadId - mine - position - }, - } - }`, - Variables: map[string]interface{}{ - "sub": query.Sub, - "sort": query.Sort, - "type": query.Type, - "cursor": query.Cursor, - "name": query.Name, - "when": query.When, - "by": query.By, - "limit": query.Limit, - }, - } - if query.Limit == 0 { - body.Variables["limit"] = 21 - } - - resp, err := MakeStackerNewsRequest(body) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - var respBody ItemsResponse - err = json.NewDecoder(resp.Body).Decode(&respBody) - if err != nil { - err = fmt.Errorf("error decoding items: %w", err) - return nil, err - } - err = CheckForErrors(respBody.Errors) - if err != nil { - return nil, err - } - return &respBody.Data.Items, nil -} - -// Create a new LINK post -func PostLink(url string, title string, sub string) (int, error) { - body := GraphQLPayload{ - Query: ` - mutation upsertLink($url: String!, $title: String!, $sub: String!) { - upsertLink(url: $url, title: $title, sub: $sub) { - id - } - }`, - Variables: map[string]interface{}{ - "url": url, - "title": title, - "sub": sub, - }, - } - resp, err := MakeStackerNewsRequest(body) - if err != nil { - return -1, err - } - defer resp.Body.Close() - - var respBody UpsertLinkResponse - err = json.NewDecoder(resp.Body).Decode(&respBody) - if err != nil { - err = fmt.Errorf("error decoding SN upsertLink: %w", err) - return -1, err - } - err = CheckForErrors(respBody.Errors) - if err != nil { - return -1, err - } - itemId := respBody.Data.UpsertLink.Id - return itemId, nil -} - -// Create a new comment -func CreateComment(parentId int, text string) (int, error) { - body := GraphQLPayload{ - Query: ` - mutation upsertComment($text: String!, $parentId: ID!) { - upsertComment(text: $text, parentId: $parentId) { - id - } - }`, - Variables: map[string]interface{}{ - "text": text, - "parentId": parentId, - }, - } - resp, err := MakeStackerNewsRequest(body) - if err != nil { - return -1, err - } - defer resp.Body.Close() - - var respBody CreateCommentsResponse - err = json.NewDecoder(resp.Body).Decode(&respBody) - if err != nil { - err = fmt.Errorf("error decoding SN createComment: %w", err) - return -1, err - } - err = CheckForErrors(respBody.Errors) - if err != nil { - return -1, err - } - - return parentId, nil -} diff --git a/main.go b/main.go deleted file mode 100644 index bbd3dcc..0000000 --- a/main.go +++ /dev/null @@ -1,81 +0,0 @@ -// Package for stacker.news API access -package sn - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "log" - "net/http" - - "github.com/joho/godotenv" - "github.com/namsral/flag" -) - -var ( - // stacker.news URL - SnUrl = "https://stacker.news" - // stacker.news API URL - SnApiUrl = "https://stacker.news/api/graphql" - // stacker.news session cookie - SnAuthCookie string - // TODO add API key support - // SnApiKey string -) - -func init() { - err := godotenv.Load() - if err != nil { - log.Fatal("error loading .env file") - } - flag.StringVar(&SnAuthCookie, "SN_AUTH_COOKIE", "", "Cookie required for authentication requests to stacker.news/api/graphql") - flag.Parse() - if SnAuthCookie == "" { - log.Fatal("SN_AUTH_COOKIE not set") - } -} - -// Make GraphQL request using raw payload -func MakeStackerNewsRequest(body GraphQLPayload) (*http.Response, error) { - bodyJSON, err := json.Marshal(body) - if err != nil { - err = fmt.Errorf("error encoding SN payload: %w", err) - return nil, err - } - - req, err := http.NewRequest("POST", SnApiUrl, bytes.NewBuffer(bodyJSON)) - if err != nil { - err = fmt.Errorf("error preparing SN request: %w", err) - return nil, err - } - req.Header.Set("Content-Type", "application/json") - - req.Header.Set("Cookie", SnAuthCookie) - - client := http.DefaultClient - resp, err := client.Do(req) - if err != nil { - err = fmt.Errorf("error posting SN payload: %w", err) - return nil, err - } - - return resp, nil -} - -// Returns error if any error was found -func CheckForErrors(graphqlErrors []GraphQLError) error { - if len(graphqlErrors) > 0 { - errorMsg, marshalErr := json.Marshal(graphqlErrors) - if marshalErr != nil { - return marshalErr - } - return errors.New(string(errorMsg)) - } - return nil -} - -// Format item id as link -func FormatLink(id int) string { - return fmt.Sprintf("%s/items/%d", SnUrl, id) -} diff --git a/notes.go b/notes.go deleted file mode 100644 index 80e42a8..0000000 --- a/notes.go +++ /dev/null @@ -1,34 +0,0 @@ -package sn - -import ( - "encoding/json" - "fmt" -) - -// Check for new notifications -func CheckNotifications() (bool, error) { - body := GraphQLPayload{ - Query: ` - { - hasNewNotes - }`, - } - resp, err := MakeStackerNewsRequest(body) - if err != nil { - return false, err - } - defer resp.Body.Close() - - var respBody HasNewNotesResponse - err = json.NewDecoder(resp.Body).Decode(&respBody) - if err != nil { - err = fmt.Errorf("error decoding SN hasNewNotes: %w", err) - return false, err - } - err = CheckForErrors(respBody.Errors) - if err != nil { - return false, err - } - - return respBody.Data.HasNewNotes, nil -} diff --git a/pkg/client.go b/pkg/client.go new file mode 100644 index 0000000..c4802c4 --- /dev/null +++ b/pkg/client.go @@ -0,0 +1,78 @@ +package sn + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" +) + +type ClientOptions struct { + BaseUrl string + ApiKey string +} + +type Client struct { + BaseUrl string + ApiUrl string + ApiKey string +} + +func NewClient(options *ClientOptions) *Client { + if options.BaseUrl == "" { + options.BaseUrl = "https://stacker.news" + } + + return &Client{ + BaseUrl: options.BaseUrl, + ApiUrl: fmt.Sprintf("%s/api/graphql", options.BaseUrl), + ApiKey: options.ApiKey, + } +} + +type GqlBody struct { + Query string `json:"query"` + Variables map[string]interface{} `json:"variables,omitempty"` +} + +type GqlError struct { + Message string `json:"message"` +} + +func (c *Client) callApi(body GqlBody) (*http.Response, error) { + bodyJSON, err := json.Marshal(body) + if err != nil { + err = fmt.Errorf("error encoding SN payload: %w", err) + return nil, err + } + + req, err := http.NewRequest("POST", c.ApiUrl, bytes.NewBuffer(bodyJSON)) + if err != nil { + err = fmt.Errorf("error preparing SN request: %w", err) + return nil, err + } + req.Header.Set("Content-Type", "application/json") + if c.ApiKey != "" { + req.Header.Set("X-Api-Key", c.ApiKey) + } + + client := http.DefaultClient + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + return resp, nil +} + +func (c *Client) checkForErrors(err []GqlError) error { + if len(err) > 0 { + errMsg, marshalErr := json.Marshal(err) + if marshalErr != nil { + return marshalErr + } + return errors.New(string(errMsg)) + } + return nil +} diff --git a/pkg/items.go b/pkg/items.go new file mode 100644 index 0000000..f642ea4 --- /dev/null +++ b/pkg/items.go @@ -0,0 +1,339 @@ +package sn + +import ( + "encoding/json" + "fmt" + "time" +) + +type Item struct { + Id int `json:"id,string"` + ParentId int `json:"parentId,string"` + Title string `json:"title"` + Url string `json:"url"` + Sats int `json:"sats"` + CreatedAt time.Time `json:"createdAt"` + Comments []Comment `json:"comments"` + NComments int `json:"ncomments"` + User User `json:"user"` +} + +type Comment struct { + Id int `json:"id,string"` + ParentId int `json:"parentId,string"` + CreatedAt time.Time `json:"createdAt"` + Text string `json:"text"` + User User `json:"user"` + Comments []Comment `json:"comments"` +} + +type ItemsQuery struct { + Sub string + Sort string + Type string + Cursor string + Name string + When string + By string + Limit int +} + +type ItemsCursor struct { + Items []Item `json:"items"` + Cursor string `json:"cursor"` +} + +type ItemsResponse struct { + Errors []GqlError `json:"errors"` + Data struct { + Items ItemsCursor `json:"items"` + } `json:"data"` +} + +type UpsertDiscussionResponse struct { + Errors []GqlError `json:"errors"` + Data struct { + UpsertDiscussion Item `json:"upsertDiscussion"` + } `json:"data"` +} + +type UpsertLinkResponse struct { + Errors []GqlError `json:"errors"` + Data struct { + UpsertLink Item `json:"upsertLink"` + } `json:"data"` +} + +type CreateCommentsResponse struct { + Errors []GqlError `json:"errors"` + Data struct { + CreateComment Comment `json:"createComment"` + } `json:"data"` +} + +type Dupe struct { + Id int `json:"id,string"` + Url string `json:"url"` + Title string `json:"title"` + User User `json:"user"` + CreatedAt time.Time `json:"createdAt"` + Sats int `json:"sats"` + NComments int `json:"ncomments"` +} + +type DupesResponse struct { + Errors []GqlError `json:"errors"` + Data struct { + Dupes []Dupe `json:"dupes"` + } `json:"data"` +} + +type DupesError struct { + Url string + Dupes []Dupe +} + +func (c *Client) Items(query *ItemsQuery) (*ItemsCursor, error) { + if query == nil { + query = &ItemsQuery{} + } + + body := GqlBody{ + Query: ` + query items($sub: String, $sort: String, $cursor: String, $type: String, $name: String, $when: String, $by: String, $limit: Limit) { + items(sub: $sub, sort: $sort, cursor: $cursor, type: $type, name: $name, when: $when, by: $by, limit: $limit) { + cursor + items { + id + parentId + createdAt + deletedAt + title + url + user { + id + name + } + otsHash + position + sats + boost + bounty + bountyPaidTo + path + upvotes + meSats + meDontLikeSats + meBookmark + meSubscription + outlawed + freebie + ncomments + commentSats + lastCommentAt + maxBid + isJob + company + location + remote + subName + pollCost + status + uploadId + mine + position + }, + } + }`, + Variables: map[string]interface{}{ + "sub": query.Sub, + "sort": query.Sort, + "type": query.Type, + "cursor": query.Cursor, + "name": query.Name, + "when": query.When, + "by": query.By, + "limit": query.Limit, + }, + } + if query.Limit == 0 { + body.Variables["limit"] = 21 + } + + resp, err := c.callApi(body) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var respBody ItemsResponse + err = json.NewDecoder(resp.Body).Decode(&respBody) + if err != nil { + err = fmt.Errorf("error decoding items: %w", err) + return nil, err + } + + err = c.checkForErrors(respBody.Errors) + if err != nil { + return nil, err + } + return &respBody.Data.Items, nil +} + +func (c *Client) PostDiscussion(title string, text string, sub string) (int, error) { + body := GqlBody{ + Query: ` + mutation upsertDiscussion($title: String!, $text: String, $sub: String) { + upsertDiscussion(title: $title, text: $text, sub: $sub) { + id + } + }`, + Variables: map[string]interface{}{ + "title": title, + "text": text, + "sub": sub, + }, + } + + resp, err := c.callApi(body) + if err != nil { + return -1, err + } + defer resp.Body.Close() + + var respBody UpsertDiscussionResponse + err = json.NewDecoder(resp.Body).Decode(&respBody) + if err != nil { + err = fmt.Errorf("error decoding upsertDiscussion: %w", err) + return -1, err + } + + err = c.checkForErrors(respBody.Errors) + if err != nil { + return -1, err + } + + return respBody.Data.UpsertDiscussion.Id, nil +} + +func (c *Client) PostLink(url string, title string, text string, sub string) (int, error) { + body := GqlBody{ + Query: ` + mutation upsertLink($url: String!, $title: String!, $text: String, $sub: String!) { + upsertLink(url: $url, title: $title, text: $text, sub: $sub) { + id + } + }`, + Variables: map[string]interface{}{ + "url": url, + "title": title, + "text": text, + "sub": sub, + }, + } + + resp, err := c.callApi(body) + if err != nil { + return -1, err + } + defer resp.Body.Close() + + var respBody UpsertLinkResponse + err = json.NewDecoder(resp.Body).Decode(&respBody) + if err != nil { + err = fmt.Errorf("error decoding upsertLink: %w", err) + return -1, err + } + + err = c.checkForErrors(respBody.Errors) + if err != nil { + return -1, err + } + + return respBody.Data.UpsertLink.Id, nil +} + +func (c *Client) CreateComment(parentId int, text string) (int, error) { + body := GqlBody{ + Query: ` + mutation upsertComment($parentId: ID!, $text: String!) { + upsertComment(parentId: $parentId, text: $text) { + id + } + }`, + Variables: map[string]interface{}{ + "parentId": parentId, + "text": text, + }, + } + + resp, err := c.callApi(body) + if err != nil { + return -1, err + } + defer resp.Body.Close() + + var respBody CreateCommentsResponse + err = json.NewDecoder(resp.Body).Decode(&respBody) + if err != nil { + err = fmt.Errorf("error decoding upsertComment: %w", err) + return -1, err + } + + err = c.checkForErrors(respBody.Errors) + if err != nil { + return -1, err + } + + return respBody.Data.CreateComment.Id, nil +} + +func (c *Client) Dupes(url string) (*[]Dupe, error) { + body := GqlBody{ + Query: ` + query Dupes($url: String!) { + dupes(url: $url) { + id + url + title + user { + name + } + createdAt + sats + ncomments + } + }`, + Variables: map[string]interface{}{ + "url": url, + }, + } + resp, err := c.callApi(body) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var respBody DupesResponse + err = json.NewDecoder(resp.Body).Decode(&respBody) + if err != nil { + err = fmt.Errorf("error decoding dupes: %w", err) + return nil, err + } + + err = c.checkForErrors(respBody.Errors) + if err != nil { + return nil, err + } + + return &respBody.Data.Dupes, nil +} + +func (c *Client) HasDupes(url string) (bool, error) { + dupes, err := c.Dupes(url) + if err != nil { + return false, err + } + + return len(*dupes) > 0, nil +} diff --git a/pkg/rss.go b/pkg/rss.go new file mode 100644 index 0000000..0663cb9 --- /dev/null +++ b/pkg/rss.go @@ -0,0 +1,70 @@ +package sn + +import ( + "encoding/xml" + "fmt" + "log" + "net/http" + "time" +) + +type RssItem struct { + Guid string `xml:"guid"` + Title string `xml:"title"` + Link string `xml:"link"` + Description string `xml:"description"` + PubDate RssDate `xml:"pubDate"` + Author RssAuthor `xml:"author"` +} + +type RssChannel struct { + Title string `xml:"title"` + Description string `xml:"description"` + Link string `xml:"link"` + Items []RssItem `xml:"item"` + LastBuildDate RssDate `xml:"lastBuildDate"` +} + +type Rss struct { + Channel RssChannel `xml:"channel"` +} + +type RssDate struct { + time.Time +} + +type RssAuthor struct { + Name string `xml:"name"` +} + +func (c *RssDate) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var v string + dateFormat := "Mon, 02 Jan 2006 15:04:05 GMT" + d.DecodeElement(&v, &start) + parse, err := time.Parse(dateFormat, v) + if err != nil { + return err + } + *c = RssDate{parse} + return nil +} + +func (c *Client) GetRssFeed() (*Rss, error) { + url := fmt.Sprintf("%s/rss", c.BaseUrl) + resp, err := http.Get(url) + if err != nil { + err = fmt.Errorf("error fetching RSS feed: %w", err) + log.Println(err) + return nil, err + } + defer resp.Body.Close() + + var rss Rss + err = xml.NewDecoder(resp.Body).Decode(&rss) + if err != nil { + err = fmt.Errorf("error decoding RSS feed XML: %w", err) + return nil, err + } + + return &rss, nil +} diff --git a/pkg/user.go b/pkg/user.go new file mode 100644 index 0000000..a5e9b70 --- /dev/null +++ b/pkg/user.go @@ -0,0 +1,6 @@ +package sn + +type User struct { + Id int `json:"id,string"` + Name string `json:"name"` +} diff --git a/rss.go b/rss.go deleted file mode 100644 index ff3c62d..0000000 --- a/rss.go +++ /dev/null @@ -1,45 +0,0 @@ -package sn - -import ( - "encoding/xml" - "fmt" - "log" - "net/http" - "time" -) - -func (c *RssDate) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { - var v string - dateFormat := "Mon, 02 Jan 2006 15:04:05 GMT" - d.DecodeElement(&v, &start) - parse, err := time.Parse(dateFormat, v) - if err != nil { - return err - } - *c = RssDate{parse} - return nil -} - -var ( - StackerNewsRssFeedUrl = "https://stacker.news/rss" -) - -// Fetch RSS feed -func RssFeed() (*Rss, error) { - resp, err := http.Get(StackerNewsRssFeedUrl) - if err != nil { - err = fmt.Errorf("error fetching RSS feed: %w", err) - log.Println(err) - return nil, err - } - defer resp.Body.Close() - - var rss Rss - err = xml.NewDecoder(resp.Body).Decode(&rss) - if err != nil { - err = fmt.Errorf("error decoding RSS feed XML: %w", err) - return nil, err - } - - return &rss, nil -} diff --git a/session.go b/session.go deleted file mode 100644 index c183803..0000000 --- a/session.go +++ /dev/null @@ -1,23 +0,0 @@ -package sn - -import ( - "fmt" - "net/http" -) - -func RefreshSession() error { - req, err := http.NewRequest("GET", SnUrl+"/api/auth/session", nil) - if err != nil { - err = fmt.Errorf("error preparing SN request: %w", err) - return err - } - req.Header.Set("Cookie", SnAuthCookie) - - client := http.DefaultClient - _, err = client.Do(req) - if err != nil { - err = fmt.Errorf("error refreshing SN session: %w", err) - return err - } - return nil -} diff --git a/types.go b/types.go deleted file mode 100644 index 1644894..0000000 --- a/types.go +++ /dev/null @@ -1,140 +0,0 @@ -package sn - -import ( - "fmt" - "time" -) - -type GraphQLPayload struct { - Query string `json:"query"` - Variables map[string]interface{} `json:"variables,omitempty"` -} - -type GraphQLError struct { - Message string `json:"message"` -} - -type User struct { - Id int `json:"id,string"` - Name string `json:"name"` -} - -type Comment struct { - Id int `json:"id,string"` - ParentId int `json:"parentId,string"` - CreatedAt time.Time `json:"createdAt"` - Text string `json:"text"` - User User `json:"user"` - Comments []Comment `json:"comments"` -} - -type CreateCommentsResponse struct { - Errors []GraphQLError `json:"errors"` - Data struct { - CreateComment Comment `json:"createComment"` - } `json:"data"` -} - -type Item struct { - Id int `json:"id,string"` - ParentId int `json:"parentId,string"` - Title string `json:"title"` - Url string `json:"url"` - Sats int `json:"sats"` - CreatedAt time.Time `json:"createdAt"` - Comments []Comment `json:"comments"` - NComments int `json:"ncomments"` - User User `json:"user"` -} - -type UpsertLinkResponse struct { - Errors []GraphQLError `json:"errors"` - Data struct { - UpsertLink Item `json:"upsertLink"` - } `json:"data"` -} - -type ItemsResponse struct { - Errors []GraphQLError `json:"errors"` - Data struct { - Items ItemsCursor `json:"items"` - } `json:"data"` -} - -type ItemsCursor struct { - Items []Item `json:"items"` - Cursor string `json:"cursor"` -} - -type HasNewNotesResponse struct { - Errors []GraphQLError `json:"errors"` - Data struct { - HasNewNotes bool `json:"hasNewNotes"` - } `json:"data"` -} - -type Dupe struct { - Id int `json:"id,string"` - Url string `json:"url"` - Title string `json:"title"` - User User `json:"user"` - CreatedAt time.Time `json:"createdAt"` - Sats int `json:"sats"` - NComments int `json:"ncomments"` -} - -type DupesResponse struct { - Errors []GraphQLError `json:"errors"` - Data struct { - Dupes []Dupe `json:"dupes"` - } `json:"data"` -} - -type DupesError struct { - Url string - Dupes []Dupe -} - -func (e *DupesError) Error() string { - return fmt.Sprintf("found %d dupes for %s", len(e.Dupes), e.Url) -} - -type RssItem struct { - Guid string `xml:"guid"` - Title string `xml:"title"` - Link string `xml:"link"` - Description string `xml:"description"` - PubDate RssDate `xml:"pubDate"` - Author RssAuthor `xml:"author"` -} - -type RssChannel struct { - Title string `xml:"title"` - Description string `xml:"description"` - Link string `xml:"link"` - Items []RssItem `xml:"item"` - LastBuildDate RssDate `xml:"lastBuildDate"` -} - -type Rss struct { - Channel RssChannel `xml:"channel"` -} - -type RssDate struct { - time.Time -} - -type RssAuthor struct { - Name string `xml:"name"` -} - -type ItemsQuery struct { - Sub string - Sort string - Type string - Cursor string - Name string - When string - By string - Limit int -}