Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: update API to 20240701 #22

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2023 Leonid Boykov
Copyright (c) 2023-2024 Leonid Boykov

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
230 changes: 39 additions & 191 deletions browse.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,8 @@ func newBrowseService(sling *sling.Sling) *BrowseService {

// DailyDeviations fetches daily deviations.
//
// To connect to this endpoint OAuth2 Access Token from the Client Credentials
// Grant, or Authorization Code Grant is required.
//
// The following scopes are required to access this resource:
//
// - browse
// To connect to this endpoint OAuth2 Access Token from the [ClientCredentials],
// or [AuthorizationCode] with a [BrowseScope] scope is required.
//
// TODO: The endpoint returns the `has_more` field, but there is no offset or
// cursor pagination information. This case requires further investigation.
Expand All @@ -46,12 +42,8 @@ func (s *BrowseService) DailyDeviations(date time.Time) (OffsetResponse[Deviatio

// DeviantsYouWatch fetches deviations of deviants you watch.
//
// To connect to this endpoint OAuth2 Access Token from the Authorization Code
// Grant is required.
//
// The following scopes are required to access this resource:
//
// - browse
// To connect to this endpoint OAuth2 Access Token from the [AuthorizationCode]
// with a [BrowseScope] is required.
func (s *BrowseService) DeviantsYouWatch(page *OffsetParams) (OffsetResponse[Deviation], error) {
var (
success OffsetResponse[Deviation]
Expand All @@ -64,6 +56,29 @@ func (s *BrowseService) DeviantsYouWatch(page *OffsetParams) (OffsetResponse[Dev
return success, nil
}

// Home browses homepage.
//
// To connect to this endpoint OAuth2 Access Token from the [AuthorizationCode]
// with a [BrowseScope] is required.
func (s *BrowseService) Home(query string, page *OffsetParams) (OffsetResponse[Deviation], error) {
type searchParams struct {
// Search query term.
//
// Estimated total results count would be available on EstimatedTotal field.
Query string `url:"q,omitempty"`
}
var (
success OffsetResponse[Deviation]
failure Error
)
params := &searchParams{Query: query}
_, err := s.sling.New().Get("home").QueryStruct(params).QueryStruct(page).Receive(&success, &failure)
if err := relevantError(err, failure); err != nil {
return OffsetResponse[Deviation]{}, fmt.Errorf("unable to fetch home deviations: %w", err)
}
return success, nil
}

type MoreLikeThisPreviewResponse struct {
Seed uuid.UUID `json:"seed"`
Author User `json:"user"`
Expand Down Expand Up @@ -100,133 +115,10 @@ func (s *BrowseService) MoreLikeThisPreview(seed uuid.UUID) (MoreLikeThisPreview
return success, nil
}

type searchParams struct {
// Search query term.
//
// Estimated total results count would be available on EstimatedTotal field.
Query string `url:"q,omitempty"`
}

// Newest fetches newest deviations.
//
// To connect to this endpoint OAuth2 Access Token from the Client Credentials
// Grant, or Authorization Code Grant is required.
//
// The following scopes are required to access this resource:
//
// - browse
func (s *BrowseService) Newest(query string, page *OffsetParams) (OffsetResponse[Deviation], error) {
var (
success OffsetResponse[Deviation]
failure Error
)
params := &searchParams{Query: query}
_, err := s.sling.New().Get("newest").QueryStruct(params).QueryStruct(page).Receive(&success, &failure)
if err := relevantError(err, failure); err != nil {
return OffsetResponse[Deviation]{}, fmt.Errorf("unable to fetch newest deviations: %w", err)
}
return success, nil
}

const (
TimeRangeNow = "now"
TimeRangeWeek = "1week"
TimeRangeMonth = "1month"
TimeRangeAll = "alltime"
)

type PopularParams struct {
// Search query term.
//
// Estimated total results count would be available on EstimatedTotal field.
Query string `url:"q,omitempty"`

// The timerange.
//
// TODO: Valid values are: values(now, 1week, 1month, alltime).
TimeRange string `url:"timerange,omitempty"`
}

// Popular fetches popular deviations.
//
// To connect to this endpoint OAuth2 Access Token from the Client Credentials
// Grant, or Authorization Code Grant is required.
//
// The following scopes are required to access this resource:
//
// - browse
//
// BUG: Query does not work properly.
// See: https://github.com/wix-incubator/DeviantArt-API/issues/206.
func (s *BrowseService) Popular(params *PopularParams, page *OffsetParams) (OffsetResponse[Deviation], error) {
var (
success OffsetResponse[Deviation]
failure Error
)
_, err := s.sling.New().Get("popular").QueryStruct(params).QueryStruct(page).Receive(&success, &failure)
if err := relevantError(err, failure); err != nil {
return OffsetResponse[Deviation]{}, fmt.Errorf("unable to fetch popular deviations: %w", err)
}
return success, nil
}

type JournalStatus struct {
Journal *Deviation `json:"journal"`
Status *Status `json:"status"`
}

// PostsDeviantsYouWatch returns deviants you watch.
//
// To connect to this endpoint OAuth2 Access Token from the Authorization Code
// Grant is required.
//
// The following scopes are required to access this resource:
//
// - browse
func (s *BrowseService) PostsDeviantsYouWatch(page *OffsetParams) (OffsetResponse[JournalStatus], error) {
var (
success OffsetResponse[JournalStatus]
failure Error
)
_, err := s.sling.New().Get("posts/deviantsyouwatch").QueryStruct(page).Receive(&success, &failure)
if err := relevantError(err, failure); err != nil {
return OffsetResponse[JournalStatus]{}, fmt.Errorf("unable to fetch deviants for you: %w", err)
}
return success, nil
}

// Recommended fetches recommended deviations.
//
// To connect to this endpoint OAuth2 Access Token from the Authorization Code
// Grant is required.
//
// The following scopes are required to access this resource:
//
// - browse
//
// TODO: Documentation specifies the `suggested_reasons` field but is absend in
// all responses. This case requires further investigation.
func (s *BrowseService) Recommended(query string) (OffsetResponse[Deviation], error) {
var (
success OffsetResponse[Deviation]
failure Error
)
params := &searchParams{Query: query}
_, err := s.sling.New().Get("recommended").QueryStruct(params).Receive(&success, &failure)
if err := relevantError(err, failure); err != nil {
return OffsetResponse[Deviation]{}, fmt.Errorf("unable to fetch recommended deviations: %w", err)
}
return success, nil
}

// Tags fetches a tag.
//
// To connect to this endpoint OAuth2 Access Token from the Client Credentials
// Grant, or Authorization Code Grant is required.
//
// The following scopes are required to access this resource:
//
// - browse
// To connect to this endpoint OAuth2 Access Token from the [ClientCredentials],
// or [AuthorizationCode] with a [BrowseScope] scope is required.
//
// NOTE: This endpoint supports cursor- and offset-base pagination.
// But for simplicity, I'll stick to cursor params for now.
Expand All @@ -247,15 +139,11 @@ func (s *BrowseService) Tags(tag string, page *CursorParams) (CursorResponse[Dev

// TagsSearch autocompletes tags.
//
// The `tag_name“ parameter should not contain spaces. If it does, spaces will
// be stripped and remainder will be treated as a single tag.
// The `tag` parameter should not contain spaces. If it does, spaces will be
// stripped and remainder will be treated as a single tag.
//
// To connect to this endpoint OAuth2 Access Token from the Client Credentials
// Grant, or Authorization Code Grant is required.
//
// The following scopes are required to access this resource:
//
// - browse
// To connect to this endpoint OAuth2 Access Token from the [ClientCredentials],
// or [AuthorizationCode] with a [BrowseScope] scope is required.
func (s *BrowseService) TagsSearch(tag string) ([]string, error) {
type tagName struct {
Name string `json:"tag_name" url:"tag_name"`
Expand All @@ -278,12 +166,8 @@ func (s *BrowseService) TagsSearch(tag string) ([]string, error) {

// Topic fetches topic deviations.
//
// To connect to this endpoint OAuth2 Access Token from the Client Credentials
// Grant, or Authorization Code Grant is required.
//
// The following scopes are required to access this resource:
//
// - browse
// To connect to this endpoint OAuth2 Access Token from the [ClientCredentials],
// or [AuthorizationCode] with a [BrowseScope] scope is required.
func (s *BrowseService) Topic(topic string, page *CursorParams) (CursorResponse[Deviation], error) {
type topicParams struct {
Topic string `url:"topic"`
Expand All @@ -307,12 +191,8 @@ type Topic struct {

// Topics fetches topics and deviations from each topic.
//
// To connect to this endpoint OAuth2 Access Token from the Client Credentials
// Grant, or Authorization Code Grant is required.
//
// The following scopes are required to access this resource:
//
// - browse
// To connect to this endpoint OAuth2 Access Token from the [ClientCredentials],
// or [AuthorizationCode] with a [BrowseScope] scope is required.
func (s *BrowseService) Topics(page *CursorParams) (CursorResponse[Topic], error) {
var (
success CursorResponse[Topic]
Expand All @@ -327,12 +207,8 @@ func (s *BrowseService) Topics(page *CursorParams) (CursorResponse[Topic], error

// Topics fetches top topics with example deviation for each one.
//
// To connect to this endpoint OAuth2 Access Token from the Client Credentials
// Grant, or Authorization Code Grant is required.
//
// The following scopes are required to access this resource:
//
// - browse
// To connect to this endpoint OAuth2 Access Token from the [ClientCredentials],
// or [AuthorizationCode] with a [BrowseScope] scope is required.
func (s *BrowseService) TopTopics(page *CursorParams) (CursorResponse[Topic], error) {
var (
success CursorResponse[Topic]
Expand All @@ -344,31 +220,3 @@ func (s *BrowseService) TopTopics(page *CursorParams) (CursorResponse[Topic], er
}
return success, nil
}

type UserJournalsParams struct {
// The username of the user to fetch journals for.
Username string `url:"username"`

// Fetch only featured or not.
Featured bool `url:"featured,omitempty"`
}

// UserJournals browses journals of a user.
//
// To connect to this endpoint OAuth2 Access Token from the Client Credentials
// Grant, or Authorization Code Grant is required.
//
// The following scopes are required to access this resource:
//
// - browse
func (s *BrowseService) UserJournals(params *UserJournalsParams, page *OffsetParams) (OffsetResponse[Deviation], error) {
var (
success OffsetResponse[Deviation]
failure Error
)
_, err := s.sling.New().Get("user/journals").QueryStruct(params).QueryStruct(page).Receive(&success, &failure)
if err := relevantError(err, failure); err != nil {
return OffsetResponse[Deviation]{}, fmt.Errorf("unable to browse user journals: %w", err)
}
return success, nil
}
12 changes: 7 additions & 5 deletions deviation.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (

"github.com/dghubble/sling"
"github.com/google/uuid"

"github.com/leonidboykov/go-deviantart/field"
)

type DeviationService struct {
Expand Down Expand Up @@ -50,9 +52,9 @@ type Deviation struct {
Comments uint32 `json:"comments"`
Favourites uint32 `json:"favourites"`
} `json:"stats,omitempty"`
PublishedTime string `json:"published_time,omitempty"`
AllowsComments bool `json:"allows_comments,omitempty"`
Tier DeviationTier `json:"tier,omitempty"`
PublishedTime field.Timestamp `json:"published_time,omitempty"`
AllowsComments bool `json:"allows_comments,omitempty"`
Tier DeviationTier `json:"tier,omitempty"`

// Preview image.
Preview StashFile `json:"preview,omitempty"`
Expand Down Expand Up @@ -127,12 +129,12 @@ type DeviationTier struct {
Settings struct {
AccessSettings string `json:"access_settings"` // TODO: enum[all,future_only,limited_past_and_future]
} `json:"settings,omitempty"`
Stats struct {
Stats field.SingleOrSlice[struct {
Subscribers uint32 `json:"subscribers,omitempty"`
Deviations uint32 `json:"deviations,omitempty"`
Posts uint32 `json:"posts,omitempty"`
Total uint32 `json:"total,omitempty"`
} `json:"stats"`
}] `json:"stats"`
Benefits []string `json:"benefits"`
}

Expand Down
18 changes: 18 additions & 0 deletions field/singleorslice.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package field

import "encoding/json"

type SingleOrSlice[T any] []T

func (f *SingleOrSlice[T]) UnmarshalJSON(data []byte) error {
var slice []T
if err := json.Unmarshal(data, &slice); err != nil {
var single T
if err := json.Unmarshal(data, &single); err != nil {
return err
}
*f = []T{single}
}
*f = slice
return nil
}
41 changes: 41 additions & 0 deletions field/timestamp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package field

import (
"fmt"
"strconv"
"time"
)

// type Timestamp time.Time

// func (t *Timestamp) UnmarshalJSON(data []byte) error {
// if data[0] == '"' && data[len(data)-1] == '"' {
// data = data[len(`"`) : len(data)-len(`"`)]
// }
// val, err := strconv.ParseInt(string(data), 10, 64)
// if err != nil {
// return fmt.Errorf("parse int: %w", err)
// }
// *t = Timestamp(time.Unix(val, 0))
// return nil
// }

// func (t Timestamp) String() string {
// return time.Time(t).String()
// }

type Timestamp struct {
time.Time
}

func (t *Timestamp) UnmarshalJSON(data []byte) error {
if data[0] == '"' && data[len(data)-1] == '"' {
data = data[len(`"`) : len(data)-len(`"`)]
}
val, err := strconv.ParseInt(string(data), 10, 64)
if err != nil {
return fmt.Errorf("parse int: %w", err)
}
t.Time = time.Unix(val, 0)
return nil
}
Loading