summaryrefslogtreecommitdiff
path: root/user-group.go
blob: d7c93584031288da50397acfd6d9a54db8dee3b1 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
package devianter

import (
	"errors"
	"strconv"
	"strings"
)

// GRuser is a profile — a user's or a group's, as DeviantArt models both the
// same way. Owner.Group distinguishes them, and determines which of the
// ModuleData fields are populated: GroupAbout and GroupAdmins for a group, the
// embedded users for a person.
//
// The Page.Modules slice mirrors the site's own profile layout, so a caller
// looking for one piece of information has to search the slice for the module
// that carries it rather than reading a field directly.
type GRuser struct {
	ErrorDescription string
	Owner            struct {
		Group    bool `json:"isGroup"`
		Username string
	}
	Gruser struct {
		ID   int `json:"gruserId"`
		Page struct {
			Modules []struct {
				Name       string
				ModuleData struct {
					GroupAbout  GroupAbout
					GroupAdmins GroupAdmins
					users
				}
			}
		}
	}
	Extra struct {
		Tag   string `json:"gruserTagline"`
		Stats struct {
			Deviations, Watchers, Watching, Pageviews, CommentsMade, Favourites, Friends int
			FeedComments                                                                 int `json:"commentsReceivedProfile"`
		}
	} `json:"pageExtraData"`
}

// Gallery is a listing of deviations from a profile, returned by
// [Group.Gallery] and [Group.Favourites].
//
// Where the deviations land depends on the call. Results is the flat listing;
// folder-scoped requests instead nest them inside the Modules slice, under
// Folder for a gallery or Folders for the folder index itself.
type Gallery struct {
	Gruser struct {
		ID   int `json:"gruserId"`
		Page struct {
			Modules []struct {
				Name       string
				ModuleData struct {
					// Folders is the index of a profile's folders, each with a
					// representative thumbnail.
					Folders struct {
						HasMore bool
						Results []struct {
							Deviations int `json:"totalItemCount"`
							FolderId   int
							Size       int
							Name       string
							Thumb      Deviation
						}
					}

					// Folder is the contents of one folder.
					Folder struct {
						HasMore    bool
						Username   string
						Pages      int `json:"totalPageCount"`
						Deviations []Deviation
					} `json:"folderDeviations"`
				}
			}
		}
	}
	HasMore bool
	Results []Deviation
}

// Group is the entry point for everything scoped to one profile. Despite the
// name it addresses users as well as groups, since DeviantArt treats the two
// alike.
//
// Name is the profile's username and must be set; the methods return an error
// otherwise. Construct it directly:
//
//	g := devianter.Group{Name: "someuser"}
//	profile, apiErr, err := g.Get()
type Group struct {
	Name    string // required
	Content Gallery
}

// Get retrieves the profile itself — its about page, statistics, and, for a
// group, its admins. It works for both users and groups; inspect
// Owner.Group on the result to tell which was returned.
func (s Group) Get() (g GRuser, daError Error, err error) {
	if s.Name == "" {
		return g, daError, errors.New("missing Name field")
	}
	daError = ujson("dauserprofile/init/about?username="+s.Name, &g)

	return
}

// Favourites retrieves a page of the profile's favourites (its collections), 50
// at a time, zero-based.
//
// Set all to gather every folder's contents into one listing. Otherwise pass a
// positive folderid to read a single folder, or 0 for the profile's default
// favourites listing.
//
// folderid is optional; omitting it is the same as passing 0. Only the first
// value is used.
func (s Group) Favourites(page int, all bool, folderid ...int) (g Group, err Error) {
	var url strings.Builder

	fid := 0
	if len(folderid) > 0 {
		fid = folderid[0]
	}

	if fid > 0 || all {
		url.WriteString("dashared/gallection/contents")
		if all {
			url.WriteString("?all_folder=true")
		} else {
			url.WriteString("?folderid=")
			url.WriteString(strconv.Itoa(fid))
		}
		url.WriteString("&type=collection&")
	} else {
		url.WriteString("dauserprofile/init/favourites?deviations_")
	}

	url.WriteString("limit=50&username=")
	url.WriteString(s.Name)
	url.WriteString("&with_subfolders=true&offset=")
	url.WriteString(strconv.Itoa(page * 50))

	err = ujson(url.String(), &g.Content)
	return
}

// Gallery retrieves a page of the profile's gallery, 50 deviations at a time.
// Pass a positive folderid to read one folder, or 0 for the whole gallery.
//
// folderid is optional; omitting it is the same as passing 0. Only the first
// value is used.
//
// Note that page is interpreted differently by the two paths this takes: the
// whole-gallery listing is zero-based, while a folder listing is one-based.
func (s Group) Gallery(page int, folderid ...int) (g Group, daError Error, err error) {
	if s.Name == "" {
		return g, daError, errors.New("missing Name field")
	}

	fid := 0
	if len(folderid) > 0 {
		fid = folderid[0]
	}

	var url strings.Builder
	if fid > 0 {
		page--
		url.WriteString("dashared/gallection/contents?username=")
		url.WriteString(s.Name)
		url.WriteString("&folderid=")
		url.WriteString(strconv.Itoa(fid))
		url.WriteString("&offset=")
		url.WriteString(strconv.Itoa(page * 50))
		url.WriteString("&type=gallery&")
	} else {
		url.WriteString("dauserprofile/init/gallery?username=")
		url.WriteString(s.Name)
		url.WriteString("&page=")
		url.WriteString(strconv.Itoa(page))
		url.WriteString("&deviations_")
	}
	url.WriteString("limit=50")
	url.WriteString("&with_subfolders=false")

	daError = ujson(url.String(), &g.Content)
	return
}

// GroupAbout is a group's about page: when it was founded and its description.
type GroupAbout struct {
	FoundatedAt timeStamp `json:"foundationTs"`
	Description Text
}

// GroupAdmins lists a group's staff. TypeId encodes each member's role
// (founder, co-founder, contributor).
type GroupAdmins struct {
	Results []struct {
		TypeId int
		User   struct {
			Username string
		}
	}
}

// About is a person's profile information, all of it self-reported and any of
// it possibly empty.
type About struct {
	Country, Website, WebsiteLabel, Gender string
	// RegDate is how long the account has existed, in seconds — an age, not a
	// registration date, despite the name.
	RegDate     int64 `json:"deviantFor"`
	Description Text  `json:"textContent"`

	SocialLinks []struct {
		Value string
	}
	Interests []struct {
		Label, Value string
	}
}

// users is the person-specific half of a profile's module data, embedded into
// [GRuser] so its fields surface inline.
type users struct {
	About          About
	CoverDeviation struct {
		Deviation Deviation `json:"coverDeviation"`
	}
}