package app import ( "encoding/base64" "encoding/json" "io" "net/http" u "net/url" "os" "strconv" "strings" "syscall" "text/template" "time" "git.macaw.me/skunky/devianter" ) // парсинг темплейтов func (s skunkyart) ExecuteTemplate(file string, data any) { var buf strings.Builder tmp := template.New(file) tmp, e := tmp.Parse(Templates[file]) err(e) err(tmp.Execute(&buf, &data)) wr(s.Writer, buf.String()) } func UrlBuilder(strs ...string) string { var str strings.Builder l := len(strs) str.WriteString(CFG.BasePath) for n, x := range strs { str.WriteString(x) if n+1 < l && !(strs[n+1][0] == '?' || strs[n+1][0] == '&') && !(x[0] == '?' || x[0] == '&') { str.WriteString("/") } } return str.String() } func (s skunkyart) ReturnHTTPError(status int) { s.Writer.WriteHeader(status) var msg strings.Builder msg.WriteString(`

`) msg.WriteString(strconv.Itoa(status)) msg.WriteString(" - ") msg.WriteString(http.StatusText(status)) msg.WriteString("

") wr(s.Writer, msg.String()) } func (s skunkyart) ConvertDeviantArtUrlToSkunkyArt(url string) (output string) { if len(url) > 32 && url[27:32] != "stash" { url = url[27:] toart := strings.Index(url, "/art/") if toart != -1 { output = UrlBuilder("post", url[:toart], url[toart+5:]) } } return } type text struct { TXT string from int to int } func ParseDescription(dscr devianter.Text) string { var parseddescription strings.Builder TagBuilder := func(tag string, content string) string { if tag != "" { var htm strings.Builder htm.WriteString("<") htm.WriteString(tag) htm.WriteString(">") htm.WriteString(content) htm.WriteString("") return htm.String() } return content } if description, dl := dscr.Html.Markup, len(dscr.Html.Markup); dl != 0 && description[0] == '{' && description[dl-1] == '}' { var descr struct { Blocks []struct { Key, Text, Type string InlineStyleRanges []struct { Offset, Length int Style string } } } e := json.Unmarshal([]byte(description), &descr) err(e) for _, x := range descr.Blocks { ranges := make(map[int]text) for i, rngs := range x.InlineStyleRanges { var tag string switch rngs.Style { case "BOLD": tag = "b" case "UNDERLINE": tag = "u" case "ITALIC": tag = "i" } fromto := rngs.Offset + rngs.Length ranges[i] = text{ TXT: TagBuilder(tag, x.Text[rngs.Offset:fromto]), from: rngs.Offset, to: fromto, } } for _, r := range ranges { var tag string switch x.Type { case "header-two": tag = "h2" case "unstyled": tag = "p" } parseddescription.WriteString(r.TXT) parseddescription.WriteString(TagBuilder(tag, x.Text[r.to:])) } } } else if dl != 0 { parseddescription.WriteString(description) } return parseddescription.String() } // навигация по страницам type dlist struct { Pages int More bool } // FIXME: на некоротрых артах первая страница может вызывать полное отсутствие панели навигации. func (s skunkyart) NavBase(c dlist) string { // TODO: сделать понятнее // навигация по страницам var list strings.Builder list.WriteString("
") p := s.Page // функция для генерации ссылок prevrev := func(msg string, page int, onpage bool) { if !onpage { list.WriteString(``) list.WriteString(msg) list.WriteString(" ") } else { list.WriteString(strconv.Itoa(page)) list.WriteString(" ") } } // вперёд-назад if p > 1 { prevrev("<= Prev |", p-1, false) } else { p = 1 } if c.Pages > 0 { // назад for x := p - 6; x < p && x > 0; x++ { prevrev(strconv.Itoa(x), x, false) } // вперёд for x := p; x <= p+6; x++ { if x == p { prevrev("", x, true) x++ } if x > p { prevrev(strconv.Itoa(x), x, false) } } } // вперёд-назад if c.More { prevrev("| Next =>", p+1, false) } return list.String() } func (s skunkyart) DeviationList(devs []devianter.Deviation, content ...dlist) string { var list strings.Builder if s.Atom && s.Page > 1 { s.ReturnHTTPError(400) return "" } else if s.Atom { list.WriteString(``) list.WriteString(`SkunkyArt`) // list.WriteString(``) } else { list.WriteString(`
`) } for _, data := range devs { if !(data.NSFW && !CFG.Nsfw) { url := s.ParseMedia(data.Media) if s.Atom { id := strconv.Itoa(data.ID) list.WriteString(``) list.WriteString(data.Author.Username) list.WriteString(``) list.WriteString(data.Title) list.WriteString(``) list.WriteString(id) list.WriteString(``) list.WriteString(data.PublishedTime.UTC().Format("Mon, 02 Jan 2006 15:04:05 -0700")) list.WriteString(``) list.WriteString(``) list.WriteString(data.Title) list.WriteString(`

`) list.WriteString(ParseDescription(data.TextContent)) list.WriteString(`

`) } else { list.WriteString(`") } } } if s.Atom { list.WriteString("") s.Writer.Write([]byte(list.String())) return "" } else { list.WriteString("
") if content != nil { list.WriteString(s.NavBase(content[0])) } } return list.String() } // FIXME: первый комментарий не отображается. func (s skunkyart) ParseComments(c devianter.Comments) string { var cmmts strings.Builder replied := make(map[int]string) cmmts.WriteString("
Comments: ") cmmts.WriteString(strconv.Itoa(c.Total)) cmmts.WriteString("") for _, x := range c.Thread { replied[x.ID] = x.User.Username cmmts.WriteString(`

`) cmmts.WriteString(x.User.Username) cmmts.WriteString(" ") if x.Parent > 0 { cmmts.WriteString(` In reply to `) if replied[x.Parent] == "" { cmmts.WriteString("???") } else { cmmts.WriteString(replied[x.Parent]) } cmmts.WriteString("") } cmmts.WriteString(" [") cmmts.WriteString(x.Posted.UTC().String()) cmmts.WriteString("]

") cmmts.WriteString(x.Comment) cmmts.WriteString("

👍: ") cmmts.WriteString(strconv.Itoa(x.Likes)) cmmts.WriteString(" ⏩: ") cmmts.WriteString(strconv.Itoa(x.Replies)) cmmts.WriteString("

\n") } cmmts.WriteString(s.NavBase(dlist{ Pages: 0, More: c.HasMore, })) cmmts.WriteString("
") return cmmts.String() } func (s skunkyart) ParseMedia(media devianter.Media) string { url := devianter.UrlFromMedia(media) if len(url) != 0 { url = url[21:] dot := strings.Index(url, ".") return UrlBuilder("media", "file", url[:dot], "/", url[dot+10:]) } return "" } func (s skunkyart) DownloadAndSendMedia(subdomain, path string) { var url strings.Builder url.WriteString("https://images-wixmp-") url.WriteString(subdomain) url.WriteString(".wixmp.com/") url.WriteString(path) url.WriteString("?token=") url.WriteString(s.Args.Get("token")) download := func() (body []byte, status int, headers http.Header) { cli := &http.Client{} if CFG.WixmpProxy != "" { u, e := u.Parse(CFG.WixmpProxy) err(e) cli.Transport = &http.Transport{Proxy: http.ProxyURL(u)} } req, e := http.NewRequest("GET", url.String(), nil) err(e) req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0.0") resp, e := cli.Do(req) err(e) defer resp.Body.Close() b, e := io.ReadAll(resp.Body) err(e) return b, resp.StatusCode, resp.Header } if CFG.Cache.Enabled { os.Mkdir(CFG.Cache.Path, 0700) fname := CFG.Cache.Path + "/" + base64.StdEncoding.EncodeToString([]byte(subdomain+path)) file, e := os.Open(fname) if e != nil { b, status, headers := download() if status == 200 && headers["Content-Type"][0][:5] == "image" { err(os.WriteFile(fname, b, 0700)) s.Writer.Write(b) } } else { file, e := io.ReadAll(file) err(e) s.Writer.Write(file) } } else if CFG.Proxy { b, _, _ := download() s.Writer.Write(b) } else { s.Writer.WriteHeader(403) s.Writer.Write([]byte("Sorry, butt proxy on this instance disabled.")) } } func InitCacheSystem() { c := &CFG.Cache for { dir, e := os.Open(c.Path) err(e) stat, e := dir.Stat() err(e) dirnames, e := dir.Readdirnames(-1) err(e) for _, a := range dirnames { a = c.Path + "/" + a rm := func() { err(os.RemoveAll(a)) } if c.Lifetime != 0 { now := time.Now().UnixMilli() f, _ := os.Stat(a) stat := f.Sys().(*syscall.Stat_t) time := time.Unix(stat.Ctim.Unix()).UnixMilli() if time+c.Lifetime <= now { rm() } } if c.MaxSize != 0 && stat.Size() > c.MaxSize { rm() } } dir.Close() time.Sleep(time.Second * time.Duration(CFG.Cache.UpdateInterval)) } } func CopyTemplatesToMemory() { try := func(e error) { if e != nil { panic(e.Error()) } } dir, e := os.ReadDir(CFG.TemplatesDir) try(e) for _, x := range dir { n := CFG.TemplatesDir + "/" + x.Name() file, e := os.ReadFile(n) try(e) Templates[n] = string(file) } }