krz/skunky-art
Alternative privacy frontend for DeviantArt.
clone: git clone https://gitbay.org/krz/skunky-art.git
1package app
2
3import (
4 "regexp"
5 "strconv"
6 "strings"
7 "time"
8
9 "github.com/krazywarez/devianter"
10 "golang.org/x/net/html"
11)
12
13// GRUser renders a group or user page: the about tab, the gallery, or favourites,
14// selected by the request's type argument.
15func (s skunkyart) GRUser() {
16 if len(s.Query) < 1 {
17 s.ReturnHTTPError(400)
18 return
19 }
20
21 var g devianter.Group
22 var daError devianter.Error
23 g.Name = s.Query
24 var err error
25 s.Templates.GroupUser.GR, daError, err = g.Get()
26 try(err)
27 if daError.RAW != nil {
28 s.Error(daError)
29 return
30 }
31
32 group := &s.Templates.GroupUser
33
34 switch s.Type {
35 case 'a':
36 g := group.GR
37 s.Atom = false
38 for _, x := range g.Gruser.Page.Modules {
39 switch x.Name {
40 case "about", "group_about":
41 if g.Owner.Group {
42 var about = &x.ModuleData.GroupAbout
43 group.Group = true
44 group.CreationDate = x.ModuleData.GroupAbout.FoundatedAt.UTC().String()
45 group.About.DescriptionFormatted = ParseDescription(s.Host, about.Description)
46 } else if false {
47 group.About.A = x.ModuleData.About
48 var about = &group.About.A
49 group.CreationDate = time.Unix(time.Now().Unix()-x.ModuleData.About.RegDate, 0).UTC().String()
50 group.About.DescriptionFormatted = ParseDescription(s.Host, about.Description)
51
52 for _, val := range x.ModuleData.About.SocialLinks {
53 var social strings.Builder
54 social.WriteString(`<a target="_blank" href="`)
55 social.WriteString(val.Value)
56 social.WriteString(`">`)
57 social.WriteString(val.Value)
58 social.WriteString("</a><br>")
59 group.About.Social += social.String()
60 }
61
62 for _, val := range x.ModuleData.About.Interests {
63 var interest strings.Builder
64 interest.WriteString(val.Label)
65 interest.WriteString(": <b>")
66 interest.WriteString(val.Value)
67 interest.WriteString("</b><br>")
68 group.About.Interests += interest.String()
69 }
70 }
71 group.About.Comments = s.ParseComments(devianter.GetComments(strconv.Itoa(group.GR.Gruser.ID), "", s.Page, 4))
72
73 case "cover_deviation":
74 group.About.BGMeta = x.ModuleData.CoverDeviation.Deviation
75 group.About.BGMeta.Url = ConvertDeviantArtURLToSkunkyArt(s.Host, group.About.BGMeta.Url)
76 group.About.BG = ParseMedia(s.Host, group.About.BGMeta.Media)
77 case "group_admins":
78 var htm strings.Builder
79 for _, z := range x.ModuleData.GroupAdmins.Results {
80 htm.WriteString(BuildUserPlate(s.Host, z.User.Username))
81 }
82 group.Admins += htm.String()
83 }
84
85 }
86 case 'g', 'f':
87 var all bool
88 var content devianter.Group
89
90 folderid, _ := strconv.Atoi(s.Args.Get("folder"))
91
92 if s.Args.Get("all") == "true" {
93 all = true
94 }
95
96 if s.Page == 0 {
97 s.Page++
98 }
99
100 if s.Type == 'f' {
101 content, daError = g.Favourites(s.Page, all, folderid)
102 } else {
103 content, daError, err = g.Gallery(s.Page, folderid)
104 try(err)
105 }
106
107 if daError.RAW != nil {
108 s.Error(daError)
109 return
110 }
111
112 if folderid > 0 || (s.Type == 'f' && all) {
113 group.Gallery.List = s.DeviationList(content.Content.Results, true, DeviationList{
114 More: content.Content.HasMore,
115 })
116 } else {
117 for _, x := range content.Content.Gruser.Page.Modules {
118 if len(x.ModuleData.Folders.Results) != 0 {
119 var folders strings.Builder
120 folders.WriteString(`<h1 id="folders"><a href="#folder">#</a> Folders</h1><div class="folders"><br>`)
121 for _, x := range x.ModuleData.Folders.Results {
122 if x.FolderId != -1 && x.Size != 0 {
123 folders.WriteString(`<div class="block folder-item">`)
124
125 if !x.Thumb.NSFW || CFG.Nsfw {
126 folders.WriteString(`<a href="`)
127 folders.WriteString(ConvertDeviantArtURLToSkunkyArt(s.Host, x.Thumb.Url))
128 folders.WriteString(`"><img loading="lazy" src="`)
129 folders.WriteString(ParseMedia(s.Host, x.Thumb.Media))
130 folders.WriteString(`" title="`)
131 folders.WriteString(x.Thumb.Title)
132 folders.WriteString(`"></a>`)
133 } else {
134 folders.WriteString(`<h1>[ <span class="nsfw">NSFW</span> ]</h1>`)
135 }
136 folders.WriteString("<br>")
137
138 folders.WriteString(`<a href="group_user?folder=`)
139 folders.WriteString(strconv.Itoa(x.FolderId))
140 folders.WriteString("&q=")
141 folders.WriteString(s.Query)
142 folders.WriteString("&type=")
143 folders.WriteRune(s.Type)
144 folders.WriteString(`">`)
145 folders.WriteString(x.Name)
146 folders.WriteString(`</a>`)
147
148 folders.WriteString("</div>")
149 }
150 }
151 folders.WriteString(`</div><h1 id="content"><a href="#content">#</a> Content</h1>`)
152 group.Gallery.Folders = folders.String()
153 }
154
155 if x.Name == "folder_deviations" {
156 group.Gallery.List = s.DeviationList(x.ModuleData.Folder.Deviations, true, DeviationList{
157 Pages: x.ModuleData.Folder.Pages,
158 More: x.ModuleData.Folder.HasMore,
159 })
160 }
161 }
162 }
163 default:
164 s.ReturnHTTPError(400)
165 }
166
167 if !s.Atom {
168 s.ExecuteTemplate("gruser.htm", "html", &s)
169 }
170}
171
172// Deviation renders a single artwork page, with its description, tags, comments
173// and related work. It responds 403 for NSFW posts on instances that disallow them.
174func (s skunkyart) Deviation(author, postname string) {
175 idSearch := regexp.MustCompile("[0-9]+").FindAllString(postname, -1)
176 if len(idSearch) < 1 {
177 s.ReturnHTTPError(400)
178 return
179 }
180
181 var err devianter.Error
182 post := &s.Templates.Deviation
183
184 id := idSearch[len(idSearch)-1]
185 post.Post, err = devianter.GetDeviation(id, author)
186 if err.RAW != nil {
187 s.Error(err)
188 return
189 }
190
191 if post.Post.Deviation.NSFW && !CFG.Nsfw {
192 s.Writer.WriteHeader(403)
193 wr(s.Writer, `<html><link rel="stylesheet" href="`+
194 URLBuilder(s.Host, "stylesheet")+
195 `" /><h1>NSFW content are disabled on this instance.</h1></html>`)
196 return
197 }
198
199 if post.Post.Comments.Total <= 50 {
200 post.Post.Comments.Cursor = ""
201 }
202
203 if post.Post.Deviation.TextContent.Excerpt != "" {
204 post.Post.Description = ParseDescription(s.Host, post.Post.Deviation.TextContent)
205 } else {
206 post.Post.Description = ParseDescription(s.Host, post.Post.Deviation.Extended.DescriptionText)
207 }
208
209 for _, x := range post.Post.Deviation.Extended.RelatedContent {
210 if len(x.Deviations) != 0 {
211 post.Related += s.DeviationList(x.Deviations, false)
212 }
213 }
214
215 // hashtags
216 for _, x := range post.Post.Deviation.Extended.Tags {
217 var tag strings.Builder
218 tag.WriteString(` <a href="`)
219 tag.WriteString(URLBuilder(s.Host, "search", "?q=", x.Name, "&type=tag"))
220 tag.WriteString(`">#`)
221 tag.WriteString(x.Name)
222 tag.WriteString("</a>")
223
224 post.Tags += tag.String()
225 }
226
227 post.Comments = s.ParseComments(devianter.GetComments(id, post.Post.Comments.Cursor, s.Page, 1))
228 post.StringTime = post.Post.Deviation.PublishedTime.UTC().String()
229 post.Post.IMG = ParseMedia(s.Host, post.Post.Deviation.Media)
230
231 s.ExecuteTemplate("deviantion.htm", "html", &s)
232}
233
234// DD renders the Daily Deviations page, including each themed strip.
235func (s skunkyart) DD() {
236 dd, err := devianter.GetDailyDeviations(s.Page)
237 if err.RAW != nil {
238 s.Error(err)
239 return
240 }
241 var strips strings.Builder
242 for _, x := range dd.Strips {
243 strips.WriteString(`<h3 class="`)
244 strips.WriteString(x.Codename)
245 strips.WriteString(`"> <a href="#`)
246 strips.WriteString(x.Codename)
247 strips.WriteString(`"># </a>`)
248 strips.WriteString(x.Title)
249 strips.WriteString(`</h3>`)
250
251 strips.WriteString(s.DeviationList(x.Deviations, false))
252 }
253 s.Templates.DDStrips = strips.String()
254 s.Templates.SomeList = s.DeviationList(dd.Deviations, true, DeviationList{
255 Pages: 0,
256 More: dd.HasMore,
257 })
258 if !s.Atom {
259 s.ExecuteTemplate("daily.htm", "html", &s)
260 }
261}
262
263// Search renders search results for the request's query. Group search is scraped
264// rather than fetched from the API, which DeviantArt does not expose to guests.
265func (s skunkyart) Search() {
266 if s.Query == "" {
267 s.ReturnHTTPError(400)
268 return
269 }
270
271 var err error
272 var daError devianter.Error
273 ss := &s.Templates.Search
274 switch s.Type {
275 case 'a', 't':
276 ss.Content, daError, err = devianter.PerformSearch(s.Query, s.Page, s.Type)
277 case 'g', 'f':
278 ss.Content, daError, err = devianter.PerformSearch(s.Query, s.Page, s.Type, s.Args.Get("usr"))
279 case 'r': // scraper, since DeviantArt withholds the guest API for group search
280 var (
281 usernames = make(map[int]string)
282 url strings.Builder
283 num int
284 )
285
286 s.Page++
287
288 url.WriteString("https://www.deviantart.com/groups/?q=")
289 url.WriteString(s.Query)
290 if s.Page > 1 {
291 url.WriteString("&offset=")
292 url.WriteString(strconv.Itoa(10 * s.Page))
293 }
294
295 dwnld := Download(url.String())
296
297 for z := html.NewTokenizer(strings.NewReader(string(dwnld.Body))); ; {
298 if n, token := z.Next(), z.Token(); n == html.StartTagToken && token.Data == "a" {
299 for _, x := range token.Attr {
300 if x.Key == "class" && x.Val == "u regular username" {
301 usernames[num] = GetValueOfTag(z)
302 num++
303 }
304 }
305 } else if n == 0 {
306 break
307 } else {
308 continue
309 }
310 }
311
312 if len(usernames) != 0 {
313 ss.List += `<div class="content plates">`
314 for x := range len(usernames) {
315 ss.List += BuildUserPlate(s.Host, usernames[x])
316 }
317 ss.List += `</div>`
318 ss.List += s.NavBase(DeviationList{
319 More: true,
320 })
321 }
322 default:
323 s.ReturnHTTPError(400)
324 return
325 }
326 try(err)
327
328 if s.Type != 'r' {
329 if daError.RAW != nil {
330 s.Error(daError)
331 return
332 }
333
334 ss.List = s.DeviationList(ss.Content.Results, false, DeviationList{
335 Pages: ss.Content.Pages,
336 More: ss.Content.HasMore,
337 })
338 }
339
340 s.ExecuteTemplate("search.htm", "html", &s)
341}
342
343// Emojitar proxies a user's avatar or emoji image, selected by the request's
344// type argument.
345func (s skunkyart) Emojitar(name string) {
346 if name == "" || (s.Type != 'a' && s.Type != 'e') {
347 s.ReturnHTTPError(400)
348 return
349 }
350
351 ae, e := devianter.AEmedia(name, s.Type)
352 if e != nil {
353 s.ReturnHTTPError(404)
354 }
355 wr(s.Writer, ae)
356}