package app
import (
"crypto/sha1" //nolint:gosec // G505: SHA-1 is a cache-key hash here, not a security primitive
"html/template"
"maps"
"net/url"
"regexp"
"strconv"
"strings"
"time"
"github.com/krazywarez/devianter"
"golang.org/x/net/html"
)
// devianter calls behind variables so tests can count and script them.
var (
fetchDeviation = devianter.GetDeviation
fetchComments = devianter.GetComments
fetchProfile = func(name string) (devianter.GRuser, devianter.Error, error) {
g := devianter.Group{Name: name}
return g.Get()
}
)
// groupSearchURL is the DeviantArt group search page for query, paged ten
// results at a time. page counts from 1; 0 means the first page too.
func groupSearchURL(query string, page int) string {
var url strings.Builder
url.WriteString("https://www.deviantart.com/groups/?q=")
url.WriteString(query)
if page > 1 {
url.WriteString("&offset=")
url.WriteString(strconv.Itoa(10 * (page - 1)))
}
return url.String()
}
// commentsOrLink renders a comment thread only when the request asked for it
// with ?comments=1, and otherwise a link that does. A thread is a second
// upstream call on every post and profile view, and most viewers never open
// it. total is shown in the link when it is known (0 or more).
func (s skunkyart) commentsOrLink(id, cursor string, kind, total int) template.HTML {
if s.Args.Get("comments") != "" {
return template.HTML(s.ParseComments(fetchComments(id, cursor, s.Page, kind))) //nolint:gosec // G203: ParseComments escapes its input
}
args := url.Values{}
maps.Copy(args, s.Args)
args.Del("p")
args.Set("comments", "1")
var link strings.Builder
link.WriteString(`
`)
strips.WriteString(s.DeviationList(x.Deviations, false))
}
s.Templates.DDStrips = template.HTML(strips.String()) //nolint:gosec // G203: escaped above
s.Templates.SomeList = template.HTML(s.DeviationList(dd.Deviations, true, DeviationList{ //nolint:gosec // G203: DeviationList escapes its input
Pages: 0,
More: dd.HasMore,
}))
if !s.Atom {
s.ExecuteTemplate("daily.htm", "html", &s)
}
}
// Search renders search results for the request's query. Group search is scraped
// rather than fetched from the API, which DeviantArt does not expose to guests.
func (s skunkyart) Search() {
if s.Query == "" {
s.ReturnHTTPError(400)
return
}
var err error
var daError devianter.Error
ss := &s.Templates.Search
switch s.Type {
case 'a', 't':
ss.Content, daError, err = devianter.PerformSearch(s.Query, s.Page, s.Type)
case 'g', 'f':
ss.Content, daError, err = devianter.PerformSearch(s.Query, s.Page, s.Type, s.Args.Get("usr"))
case 'r': // scraper, since DeviantArt withholds the guest API for group search
var (
usernames = make(map[int]string)
num int
)
dwnld := Download(groupSearchURL(s.Query, s.Page))
for z := html.NewTokenizer(strings.NewReader(string(dwnld.Body))); ; {
if n, token := z.Next(), z.Token(); n == html.StartTagToken && token.Data == "a" {
for _, x := range token.Attr {
if x.Key == "class" && x.Val == "u regular username" {
usernames[num] = GetValueOfTag(z)
num++
}
}
} else if n == 0 {
break
} else {
continue
}
}
if len(usernames) != 0 {
var plates strings.Builder
plates.WriteString(`
`)
for x := range len(usernames) {
plates.WriteString(BuildUserPlate(s.Host, usernames[x]))
}
plates.WriteString(`
`)
plates.WriteString(s.NavBase(DeviationList{
More: true,
}))
ss.List = template.HTML(plates.String()) //nolint:gosec // G203: BuildUserPlate escapes its input
}
default:
s.ReturnHTTPError(400)
return
}
try(err)
if s.Type != 'r' {
if daError.RAW != nil {
s.Error(daError)
return
}
ss.List = template.HTML(s.DeviationList(ss.Content.Results, false, DeviationList{ //nolint:gosec // G203: DeviationList escapes its input
Pages: ss.Content.Pages,
More: ss.Content.HasMore,
}))
}
s.ExecuteTemplate("search.htm", "html", &s)
}
// fetchAvatar is devianter.AEmedia behind a variable so tests can count calls.
var fetchAvatar = devianter.AEmedia
// Emojitar proxies a user's avatar or emoji image, selected by the request's
// type argument. With the media cache on, the image is served from it after
// the first fetch: avatars are on every comment and listing, and DeviantArt
// answers each fetch with up to three requests.
func (s skunkyart) Emojitar(name string) {
if name == "" || (s.Type != 'a' && s.Type != 'e') {
s.ReturnHTTPError(400)
return
}
key := sha1.Sum([]byte("emojitar:" + string(s.Type) + ":" + strings.ToLower(name))) //nolint:gosec // G401: cache key, not a security primitive
if CFG.Cache.Enabled {
if body := cachedBody(key); body != nil {
_, _ = s.Writer.Write(body)
return
}
}
ae, e := fetchAvatar(name, s.Type)
if e != nil {
s.ReturnHTTPError(404)
return
}
if CFG.Cache.Enabled {
storeBody(key, []byte(ae))
}
wr(s.Writer, ae)
}