krz/devianter
clone: git clone https://gitbay.org/krz/devianter.git
main: user-group.go · raw
1package devianter
2
3import (
4 "errors"
5 "strconv"
6 "strings"
7)
8
9// GRuser is a profile — a user's or a group's, as DeviantArt models both the
10// same way. Owner.Group distinguishes them, and determines which of the
11// ModuleData fields are populated: GroupAbout and GroupAdmins for a group, the
12// embedded users for a person.
13//
14// The Page.Modules slice mirrors the site's own profile layout, so a caller
15// looking for one piece of information has to search the slice for the module
16// that carries it rather than reading a field directly.
17type GRuser struct {
18 ErrorDescription string
19 Owner struct {
20 Group bool `json:"isGroup"`
21 Username string
22 }
23 Gruser struct {
24 ID int `json:"gruserId"`
25 Page struct {
26 Modules []struct {
27 Name string
28 ModuleData struct {
29 GroupAbout GroupAbout
30 GroupAdmins GroupAdmins
31 users
32 }
33 }
34 }
35 }
36 Extra struct {
37 Tag string `json:"gruserTagline"`
38 Stats struct {
39 Deviations, Watchers, Watching, Pageviews, CommentsMade, Favourites, Friends int
40 FeedComments int `json:"commentsReceivedProfile"`
41 }
42 } `json:"pageExtraData"`
43}
44
45// Gallery is a listing of deviations from a profile, returned by
46// [Group.Gallery] and [Group.Favourites].
47//
48// Where the deviations land depends on the call. Results is the flat listing;
49// folder-scoped requests instead nest them inside the Modules slice, under
50// Folder for a gallery or Folders for the folder index itself.
51type Gallery struct {
52 Gruser struct {
53 ID int `json:"gruserId"`
54 Page struct {
55 Modules []struct {
56 Name string
57 ModuleData struct {
58 // Folders is the index of a profile's folders, each with a
59 // representative thumbnail.
60 Folders struct {
61 HasMore bool
62 Results []struct {
63 Deviations int `json:"totalItemCount"`
64 FolderId int
65 Size int
66 Name string
67 Thumb Deviation
68 }
69 }
70
71 // Folder is the contents of one folder.
72 Folder struct {
73 HasMore bool
74 Username string
75 Pages int `json:"totalPageCount"`
76 Deviations []Deviation
77 } `json:"folderDeviations"`
78 }
79 }
80 }
81 }
82 HasMore bool
83 Results []Deviation
84}
85
86// Group is the entry point for everything scoped to one profile. Despite the
87// name it addresses users as well as groups, since DeviantArt treats the two
88// alike.
89//
90// Name is the profile's username and must be set; the methods return an error
91// otherwise. Construct it directly:
92//
93// g := devianter.Group{Name: "someuser"}
94// profile, apiErr, err := g.Get()
95type Group struct {
96 Name string // required
97 Content Gallery
98}
99
100// Get retrieves the profile itself — its about page, statistics, and, for a
101// group, its admins. It works for both users and groups; inspect
102// Owner.Group on the result to tell which was returned.
103func (s Group) Get() (g GRuser, daError Error, err error) {
104 if s.Name == "" {
105 return g, daError, errors.New("missing Name field")
106 }
107 daError = ujson("dauserprofile/init/about?username="+s.Name, &g)
108
109 return
110}
111
112// Favourites retrieves a page of the profile's favourites (its collections), 50
113// at a time, zero-based.
114//
115// Set all to gather every folder's contents into one listing. Otherwise pass a
116// positive folderid to read a single folder, or 0 for the profile's default
117// favourites listing.
118//
119// folderid is optional; omitting it is the same as passing 0. Only the first
120// value is used.
121func (s Group) Favourites(page int, all bool, folderid ...int) (g Group, err Error) {
122 var url strings.Builder
123
124 fid := 0
125 if len(folderid) > 0 {
126 fid = folderid[0]
127 }
128
129 if fid > 0 || all {
130 url.WriteString("dashared/gallection/contents")
131 if all {
132 url.WriteString("?all_folder=true")
133 } else {
134 url.WriteString("?folderid=")
135 url.WriteString(strconv.Itoa(fid))
136 }
137 url.WriteString("&type=collection&")
138 } else {
139 url.WriteString("dauserprofile/init/favourites?deviations_")
140 }
141
142 url.WriteString("limit=50&username=")
143 url.WriteString(s.Name)
144 url.WriteString("&with_subfolders=true&offset=")
145 url.WriteString(strconv.Itoa(page * 50))
146
147 err = ujson(url.String(), &g.Content)
148 return
149}
150
151// Gallery retrieves a page of the profile's gallery, 50 deviations at a time.
152// Pass a positive folderid to read one folder, or 0 for the whole gallery.
153//
154// folderid is optional; omitting it is the same as passing 0. Only the first
155// value is used.
156//
157// Note that page is interpreted differently by the two paths this takes: the
158// whole-gallery listing is zero-based, while a folder listing is one-based.
159func (s Group) Gallery(page int, folderid ...int) (g Group, daError Error, err error) {
160 if s.Name == "" {
161 return g, daError, errors.New("missing Name field")
162 }
163
164 fid := 0
165 if len(folderid) > 0 {
166 fid = folderid[0]
167 }
168
169 var url strings.Builder
170 if fid > 0 {
171 page--
172 url.WriteString("dashared/gallection/contents?username=")
173 url.WriteString(s.Name)
174 url.WriteString("&folderid=")
175 url.WriteString(strconv.Itoa(fid))
176 url.WriteString("&offset=")
177 url.WriteString(strconv.Itoa(page * 50))
178 url.WriteString("&type=gallery&")
179 } else {
180 url.WriteString("dauserprofile/init/gallery?username=")
181 url.WriteString(s.Name)
182 url.WriteString("&page=")
183 url.WriteString(strconv.Itoa(page))
184 url.WriteString("&deviations_")
185 }
186 url.WriteString("limit=50")
187 url.WriteString("&with_subfolders=false")
188
189 daError = ujson(url.String(), &g.Content)
190 return
191}
192
193// GroupAbout is a group's about page: when it was founded and its description.
194type GroupAbout struct {
195 FoundatedAt timeStamp `json:"foundationTs"`
196 Description Text
197}
198
199// GroupAdmins lists a group's staff. TypeId encodes each member's role
200// (founder, co-founder, contributor).
201type GroupAdmins struct {
202 Results []struct {
203 TypeId int
204 User struct {
205 Username string
206 }
207 }
208}
209
210// About is a person's profile information, all of it self-reported and any of
211// it possibly empty.
212type About struct {
213 Country, Website, WebsiteLabel, Gender string
214 // RegDate is how long the account has existed, in seconds — an age, not a
215 // registration date, despite the name.
216 RegDate int64 `json:"deviantFor"`
217 Description Text `json:"textContent"`
218
219 SocialLinks []struct {
220 Value string
221 }
222 Interests []struct {
223 Label, Value string
224 }
225}
226
227// users is the person-specific half of a profile's module data, embedded into
228// [GRuser] so its fields surface inline.
229type users struct {
230 About About
231 CoverDeviation struct {
232 Deviation Deviation `json:"coverDeviation"`
233 }
234}