commit 883b4880e5438e00f54681657b7311f73d79f600
Author: Skunky <>
Date: Thu Apr 4 21:24:47 2024 +0300
First commit!
diff --git a/config.json b/config.json
new file mode 100644
index 0000000..ce29bac
--- /dev/null
+++ b/config.json
@@ -0,0 +1,11 @@
+{
+ "listen": "0.0.0.0:3003",
+ "cache": {
+ "enabled": true,
+ "path": "cache",
+ "lifetime": null,
+ "max_size": 10000
+ },
+ "proxy": false,
+ "nsfw": false
+}
\ No newline at end of file
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..f2b1082
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,5 @@
+module skunkyart
+
+go 1.18
+
+require golang.org/x/net v0.21.0
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..dab1c7f
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,2 @@
+golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..91520b4
--- /dev/null
+++ b/main.go
@@ -0,0 +1,108 @@
+package main
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "os"
+ "runtime"
+ "strconv"
+ "strings"
+ "time"
+
+ "skunkyart/misc"
+ "skunkyart/post"
+ "skunkyart/user"
+ "skunkyart/util"
+)
+
+const Compiler = "gccgo"
+
+func main() {
+ util.ParseConfig()
+ print("Getting CSRF Token..\033[B\r")
+ util.CSRFToken()
+ print("CSRF Token has been getted.\033[B\r\n")
+
+ go func() {
+ for {
+ time.Sleep(30 * time.Minute)
+ util.CSRFToken()
+ println("CSRF token has been updated.")
+ }
+ }()
+
+ println("SkunkyArt listening on http://" + util.Conf.Listen)
+
+ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ e, p := endpoint(r.URL.Path)
+ c := make(chan string, 100)
+
+ switch e {
+ case "image":
+ fmt.Fprintln(w, misc.Getimage(p, url.QueryEscape(r.URL.Query().Get("t"))))
+
+ case "avatar":
+ if p == "" {
+ w.WriteHeader(400)
+ fmt.Fprint(w, "Missing username.")
+ } else {
+ fmt.Fprintln(w, misc.Misc(strings.ToLower(p), false))
+ }
+
+ case "emoji":
+ fmt.Fprintln(w, misc.Misc(strings.ToLower(p), true))
+
+ case "group":
+ fmt.Fprintln(w, "Groups is not implemented yet :(")
+
+ case "user":
+ fmt.Fprintln(w, user.Get(p, r.URL.Query().Get("a"), r.URL.Query().Get("p"), url.QueryEscape(r.URL.Query().Get("q"))))
+
+ case "post":
+ intt, _ := strconv.ParseInt(r.URL.Query().Get("page"), 10, 32)
+
+ go post.Get(p, int(intt)+1, c)
+ fmt.Fprintln(w, <-c)
+
+ case "dd":
+ intt, _ := strconv.ParseInt(r.URL.Query().Get("p"), 10, 32)
+
+ go misc.DD(int(intt), c)
+ fmt.Fprintln(w, <-c)
+
+ case "search":
+ go misc.Search(url.QueryEscape(r.URL.Query().Get("q")), r.URL.Query().Get("p"), r.URL.Query().Get("scope"), c)
+ fmt.Fprintln(w, <-c)
+
+ case "about":
+ fmt.Fprintln(w, "Its an alternative frontend for deviantart.com. Compiled with Go:", runtime.Version())
+
+ case "static":
+ file, err := os.ReadFile("static/" + p)
+ if err != nil {
+ fmt.Fprintln(w, "No such file")
+ } else {
+ w.Header().Add("Content-Type", "text/css")
+ fmt.Fprintln(w, string(file))
+ }
+
+ case "":
+ f, _ := os.ReadFile("templates/home.htm")
+ fmt.Fprintln(w, string(f))
+
+ default:
+ w.WriteHeader(404)
+ fmt.Fprintln(w, "404 - Not found.")
+ }
+ })
+ http.ListenAndServe(util.Conf.Listen, nil)
+}
+
+func endpoint(url string) (string, string) {
+ end := strings.Index(url[1:], "/")
+ if end == -1 {
+ return url[1:], ""
+ }
+ return url[1 : end+1], url[end+2:]
+}
diff --git a/misc/dailydeviations.go b/misc/dailydeviations.go
new file mode 100644
index 0000000..2a29580
--- /dev/null
+++ b/misc/dailydeviations.go
@@ -0,0 +1,42 @@
+package misc
+
+import (
+ "bytes"
+ "encoding/json"
+ t "html/template"
+ "skunkyart/util"
+ "strconv"
+)
+
+func DD(page int, out chan string) {
+ var buf bytes.Buffer
+ {
+ var (
+ j struct {
+ Deviations []util.Deviation
+ }
+ d, p string
+ )
+
+ if page == 0 {
+ page++
+ }
+
+ rawjson := util.Puppy("dabrowse/networkbar/rfy/deviations?page=" + strconv.Itoa(page))
+ json.Unmarshal([]byte(rawjson), &j)
+
+ for _, a := range j.Deviations {
+ d += util.Images(a, a.Media, false)
+ }
+
+ if page > 1 {
+ p += "<-- Back "
+ }
+ p += "Next -->"
+
+ tmp, _ := t.ParseFiles("templates/dd.htm")
+ tmp.Execute(&buf, t.HTML(d))
+ tmp.ExecuteTemplate(&buf, "P", t.HTML(p))
+ }
+ out <- buf.String()
+}
diff --git a/misc/media.go b/misc/media.go
new file mode 100644
index 0000000..ecbaf4f
--- /dev/null
+++ b/misc/media.go
@@ -0,0 +1,46 @@
+package misc
+
+import (
+ "skunkyart/util"
+ "strings"
+)
+
+var Extensions = [3]string{
+ ".jpg",
+ ".png",
+ ".gif",
+}
+
+func get(scope, name string) string {
+ var uri string
+ switch scope {
+ case "avatar":
+ uri = "https://a.deviantart.net/avatars-big" + "/" + string(name[0:1]) + "/" + string(name[1:2]) + "/" + name
+ case "emoji":
+ uri = "https://e.deviantart.net/emoticons/" + name[0:1] + "/" + name
+ }
+
+ for x := 0; x < len(Extensions); x++ {
+ image := util.Cache(uri + Extensions[x])
+ if image != "Failed to get image." {
+ return image
+ }
+ }
+ return ""
+}
+
+func Misc(name string, emoji bool) string {
+ if emoji {
+ return get("emoji", name)
+ }
+ return get("avatar", name)
+}
+
+func Getimage(img string, token string) string {
+ var imguri string
+ if len(img) > 0 {
+ imguri = "https://images-wixmp-" + img[0:strings.Index(img, "/")] + ".wixmp.com" + img[strings.Index(img, "/"):] + "?token=" + token
+ return util.Cache(imguri)
+ }
+ return "Invalid uri."
+}
diff --git a/misc/search.go b/misc/search.go
new file mode 100644
index 0000000..9a16eaf
--- /dev/null
+++ b/misc/search.go
@@ -0,0 +1,72 @@
+package misc
+
+import (
+ "bytes"
+ "encoding/json"
+ "html/template"
+ "math"
+ "os"
+ "skunkyart/util"
+ "strconv"
+)
+
+func Search(q, p, scope string, output chan string) {
+ var buf bytes.Buffer
+ if q != "" {
+ var j struct {
+ EstTotal int
+ Deviations []util.Deviation
+ }
+
+ var rawjson string
+ switch scope {
+ case "", "all":
+ rawjson = util.Puppy("dabrowse/search/all?q=" + q + "&page=" + p)
+ case "tag":
+ rawjson = util.Puppy("dabrowse/networkbar/tag/deviations?tag=" + q + "&page=" + p)
+ default:
+ output <- "Unknown scope."
+ }
+
+ json.Unmarshal([]byte(rawjson), &j)
+
+ var tmp struct {
+ Results, Pages string
+ Total int
+ }
+ for _, a := range j.Deviations {
+ tmp.Results += util.Images(a, a.Media, false)
+ }
+ tmp.Total = j.EstTotal
+
+ switch scope {
+ case "all":
+ for x := 0; x < int(math.Round(float64(tmp.Total/25))); x++ {
+ if x == 417 {
+ break
+ }
+ tmp.Pages += "" + strconv.Itoa(x+1) + " "
+ }
+ case "tag":
+ if p == "" {
+ p = "1"
+ }
+ next, _ := strconv.ParseInt(p, 10, 32)
+ if int(next) > 1 {
+ tmp.Pages += "<-- Back "
+ }
+ tmp.Pages += "Next -->"
+ default:
+ output <- "Missing or invalid scope."
+ }
+
+ tmpl, _ := template.ParseFiles("templates/search/search.htm")
+ tmpl.Execute(&buf, tmp.Total)
+ tmpl.ExecuteTemplate(&buf, "R", template.HTML(tmp.Results))
+ tmpl.ExecuteTemplate(&buf, "P", template.HTML(tmp.Pages))
+ } else {
+ doc, _ := os.ReadFile("templates/search/search-base.htm")
+ output <- string(doc)
+ }
+ output <- buf.String()
+}
diff --git a/post/comments.go b/post/comments.go
new file mode 100644
index 0000000..b9cb334
--- /dev/null
+++ b/post/comments.go
@@ -0,0 +1,137 @@
+package post
+
+import (
+ "encoding/json"
+ "fmt"
+ "skunkyart/util"
+ "strconv"
+ "strings"
+)
+
+type Comments struct {
+ Total int
+ Thread []struct {
+ CommentId, ParentId, Replies, Likes int
+ Posted util.Time
+ IsAuthorHighlited bool
+
+ TextContent struct {
+ Html struct {
+ Markup string
+ }
+ }
+
+ User struct {
+ Username string
+ IsBanned bool
+ }
+ }
+}
+
+// json
+func js(c string, cc int, id []string) (*string, bool, bool) {
+ var result string
+
+ var j struct {
+ Cursor string
+ PrevOffset int
+ HasMore, HasLess bool
+ }
+
+ for x := 0; x < cc; x++ {
+ p := "dashared/comments/thread?typeid=1&itemid=" + id[len(id)-1] + "&maxdepth=1000&order=newest"
+ result = util.Puppy(p + "&limit=50&cursor=" + strings.ReplaceAll(c, "+", `%2B`))
+
+ json.Unmarshal([]byte(result), &j)
+ if !j.HasMore {
+ cc = 0
+ }
+ c = j.Cursor
+ }
+
+ return &result, j.HasMore, j.HasLess
+}
+
+// comments!!
+func comments(uri string, user string, page int, cursor string, id []string) *string {
+ var comments Comments
+ rawjson, next, prev := js(cursor, page, id)
+
+ err := json.Unmarshal([]byte(*rawjson), &comments)
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ // обработка жирсона
+ var htm string
+ for _, b := range comments.Thread {
+ var msgbody string
+ // если свич находит фигурную скобку, то парсит "по-новому", не находит - "по-старому" (в сообщении нет жсона).
+ switch b.TextContent.Html.Markup[0:1] {
+ case "{":
+ var comments struct {
+ Blocks []struct {
+ Text string
+ // EntityMap []struct {
+ // Data struct {
+ // Src, Title, Type string
+ // }
+ // }
+ }
+ }
+
+ err := json.Unmarshal([]byte(b.TextContent.Html.Markup), &comments)
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ for _, a := range comments.Blocks {
+ msgbody = a.Text
+ // for _, a := range a.EntityMap {
+ // fmt.Println(a)
+ // println("" + a.Data.Title + "")
+ // msgbody += "
" + a.Data.Title + ""
+ // }
+ }
+ default:
+ msgbody = b.TextContent.Html.Markup
+ }
+
+ // удаляем говно
+ msgbody = strings.ReplaceAll(msgbody, "https://www.deviantart.com/users/outgoing?", "")
+
+ // генерируем HTML-куски
+ var (
+ isauthor string
+ reply string
+ )
+ if b.IsAuthorHighlited {
+ isauthor = "author"
+ }
+ var space string
+ if b.ParentId != 0 {
+ space = "margin-left: 2%"
+ reply = "In reply to message:"
+ }
+
+ msgbody = util.Parse(msgbody)
+
+ htm += fmt.Sprintf("
There is no comments.
" + } + + return &htm +} diff --git a/post/main.go b/post/main.go new file mode 100644 index 0000000..235a89d --- /dev/null +++ b/post/main.go @@ -0,0 +1,141 @@ +package post + +import ( + "bytes" + "encoding/json" + "html/template" + "regexp" + "skunkyart/util" + "strconv" + "strings" +) + +type Post struct { + Deviation util.Deviation + Comments struct { + Total, Cursor int + } +} + +var tmp struct { + Author, Postname, Img, Description, Time, Tags, Comments, License, Recomendations string + Views, FavsCount, Downloads int + Nsfw, Ai, DD bool +} + +func _post(id []string, author string, url string, page int) string { + var j Post + + if !strings.Contains(url, "journal/") { + url = "art/" + url + } + + // парсинг + rawjson := util.Puppy("dadeviation/init?deviationid=" + id[len(id)-1] + "&username=" + author + "&type=art&include_session=false&expand=deviation.related&preload=true") + if strings.Contains(rawjson, "{") { + json.Unmarshal([]byte(rawjson), &j) + } else { + println("Something went wrong. Maybe, rate limit?") + } + + tmp.Nsfw = j.Deviation.IsMature + if !util.Conf.Nsfw && j.Deviation.IsMature { + return "NSFW content has been disabled on this instance." + } + + tmp.Author = j.Deviation.Author.Username + tmp.Postname = j.Deviation.Title + + tmp.Img = util.Images(j.Deviation, j.Deviation.Media, true) + + tmp.Time = j.Deviation.PublishedTime.UTC().String() + tmp.Views = j.Deviation.Stats.Views + tmp.FavsCount = j.Deviation.Stats.Favourites + + if j.Deviation.Extended.DescriptionText.Html.Markup != "" { + tmp.Description = j.Deviation.Extended.DescriptionText.Html.Markup + } else { + tmp.Description = j.Deviation.TextContent.Html.Markup + } + + if len(tmp.Description) > 0 { + if tmp.Description[0:1] == "{" { + var j2 struct { + Blocks []struct { + Text string + } + } + + json.Unmarshal([]byte(tmp.Description), &j2) + + tmp.Description = "" + for _, a := range j2.Blocks { + if j.Deviation.TextContent.Html.Type == "draft" || j.Deviation.Extended.DescriptionText.Html.Type == "draft" { + tmp.Description += a.Text + "Pages: {{.}}
+ + +{{end}} \ No newline at end of file diff --git a/templates/user/info.htm b/templates/user/info.htm new file mode 100644 index 0000000..615bc79 --- /dev/null +++ b/templates/user/info.htm @@ -0,0 +1,36 @@ + + + +Watchers: {{.Watchers}}; Watching: {{.Watching}}; Comments on Profile: {{.CMMRCPF}}; Posts: {{.Posts}}; Pageviews: {{.Pageviews}}; Comments: {{.CM}}; + Favourites: {{.Fav}}; Friends: {{.Friends}} +
+Registration Date: {{.RegDate}} +
From {{.Country}}.
+ {{.SiteTitle}} +
+{{.Status}}
+ {{define "S"}} ++ {{.}} +
+ {{end}} + {{define "I"}} +{{.}}
+ {{end}} + {{define "D"}} +{{.}}
+ + +{{end}} \ No newline at end of file diff --git a/templates/user/search-base.htm b/templates/user/search-base.htm new file mode 100644 index 0000000..50de613 --- /dev/null +++ b/templates/user/search-base.htm @@ -0,0 +1,16 @@ + + + +Pages: {{.}}
+ + +{{end}} \ No newline at end of file diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..45ddb55 --- /dev/null +++ b/todo.md @@ -0,0 +1,12 @@ +Нужно сделать: +- провести оптимизацию кода +- кеширование жсона +- ~~добавить устаревание кеша и его максимальный вес~~ +- ~~проксирование и кеширование гифок~~ +- картинки в комментариях +- улучшить парсинг HTML +- улучшить парсинг описания профиля пользователя +- реализовать RSS-ленты пользователей +- ~~добавить возможность включать/выключать разрешение просматривать NSFW-контент в конфиге~~ +- реализовать систему аккаунтов +- написать несколько тем, в том числе, легковесных \ No newline at end of file diff --git a/user/about.go b/user/about.go new file mode 100644 index 0000000..27d676b --- /dev/null +++ b/user/about.go @@ -0,0 +1,128 @@ +package user + +import ( + "bytes" + "encoding/json" + "fmt" + "html/template" + "skunkyart/util" + "time" +) + +type Tmp struct { + Watchers, Posts, Watching, Pageviews, CMMRCPF, CM, Fav, Friends int + Name, Description, Bg, Bgname, Country, RegDate, Gender, Site, SiteTitle, Social, Interests, Status string +} + +func about(name *string, output chan string) { + ums := time.Now().Unix() + + var buf bytes.Buffer + { + var tmp Tmp + var j ab + + // парсинг + rawjson := util.Puppy("dauserprofile/init/about?username=" + *name) + err := json.Unmarshal([]byte(rawjson), &j) + if err != nil { + fmt.Println(err) + if rawjson == "Something went wrong. Status: 403" { + output <- "Rate limit from Cloudfront." + } else { + output <- "Something went wrong." + } + } else { + // ошибки + switch j.ErrorDescription { + case "User deactivated.": + output <- "User deactivated their account or has been banned." + case "user_not_found.": + output <- "Not exists user." + } + j.ErrorDescription = "" + + for _, a := range j.Gruser.Page.Modules { + if a.ModuleData.About.Country != "" { + tmp.Country = a.ModuleData.About.Country + } + + switch a.Name { + case "about": + var j struct { + Blocks []struct { + Text string + // InlineStyleRanges []struct { + // Offset, Length int + // Style string + // EntityMap struct { + + // } + // } + } + } + + json.Unmarshal([]byte(a.ModuleData.About.TextContent.Html.Markup), &j) + + for _, a := range j.Blocks { + tmp.Description += a.Text + "" + } + case "cover_deviation": + tmp.Bg = util.Images(a.ModuleData.CoverDeviation.CoverDeviation, a.ModuleData.CoverDeviation.CoverDeviation.Media, true) + tmp.Bgname = a.ModuleData.CoverDeviation.CoverDeviation.Title + } + + if a.ModuleData.About.DeviantFor > 0 { + tmp.RegDate = time.Unix(ums-a.ModuleData.About.DeviantFor, 0).UTC().String() + } + + switch a.ModuleData.About.Gender { + case "female": + tmp.Gender = "♀️" + case "male": + tmp.Gender = "♂️" + } + + if a.ModuleData.About.Website != "" { + tmp.Site = a.ModuleData.About.Website + if a.ModuleData.About.WebsiteLabel != "" { + tmp.SiteTitle = a.ModuleData.About.WebsiteLabel + } else { + tmp.SiteTitle = a.ModuleData.About.Website + } + } + + for _, a := range a.ModuleData.About.SocialLinks { + tmp.Social += "" + a.Value + "
" + } + + for _, a := range a.ModuleData.About.Interests { + tmp.Interests += "" + a.Label + ": " + a.Value + "
"
+ }
+
+ if a.ModuleData.About.Tagline != "" {
+ tmp.Status = a.ModuleData.About.Tagline
+ }
+
+ // говно
+ tmp.Watchers = j.PageExtraData.Stats.Watchers
+ tmp.Posts = j.PageExtraData.Stats.Deviations
+ tmp.Watching = j.PageExtraData.Stats.Watching
+ tmp.CMMRCPF = j.PageExtraData.Stats.CommentsReceivedProfile
+ tmp.CM = j.PageExtraData.Stats.CommentsMade
+ tmp.Fav = j.PageExtraData.Stats.Favourites
+ tmp.Friends = j.PageExtraData.Stats.Friends
+ tmp.Name = j.Owner.Username
+ }
+
+ // темплейты
+ tmpl, _ := template.ParseFiles("templates/user/info.htm")
+ tmpl.Execute(&buf, tmp)
+ tmpl.ExecuteTemplate(&buf, "S", template.HTML(tmp.Social))
+ tmpl.ExecuteTemplate(&buf, "I", template.HTML(tmp.Interests))
+ tmpl.ExecuteTemplate(&buf, "D", template.HTML(tmp.Description))
+ }
+ }
+
+ output <- buf.String()
+}
diff --git a/user/gallery.go b/user/gallery.go
new file mode 100644
index 0000000..c87f479
--- /dev/null
+++ b/user/gallery.go
@@ -0,0 +1,72 @@
+package user
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "html/template"
+ "skunkyart/util"
+ "strconv"
+)
+
+func gallery(name *string, output chan string, page string) {
+ var buf bytes.Buffer
+ {
+ var tmp struct {
+ Posts, Name, Nav string
+ TP int
+ }
+ var j struct {
+ Gruser struct {
+ Page struct {
+ Modules []struct {
+ Name string
+ ModuleData struct {
+ FolderDeviations struct {
+ Username string
+ TotalPageCount int
+ Deviations []util.Deviation
+ }
+ }
+ }
+ }
+ }
+ }
+
+ rawjson := util.Puppy("dauserprofile/init/gallery?username=" + *name + "&page=" + page + "&deviations_limit=50&with_subfolders=false")
+ err := json.Unmarshal([]byte(rawjson), &j)
+ if err != nil {
+ fmt.Println(err)
+ if rawjson == "Something went wrong. Status: 403" {
+ output <- "Rate limit from CloudFront."
+ } else {
+ output <- "Something went wrong."
+ }
+ } else {
+ for _, a := range j.Gruser.Page.Modules {
+ if a.ModuleData.FolderDeviations.Username != "" {
+ tmp.Name = a.ModuleData.FolderDeviations.Username
+ }
+ if a.ModuleData.FolderDeviations.TotalPageCount != 0 {
+ tmp.TP = a.ModuleData.FolderDeviations.TotalPageCount
+ }
+
+ for _, a := range a.ModuleData.FolderDeviations.Deviations {
+ tmp.Posts += util.Images(a, a.Media, false)
+ }
+
+ tmp.Nav = ""
+ for x := 0; x < tmp.TP; x++ {
+ tmp.Nav += "" + strconv.Itoa(x+1) + " "
+ }
+ }
+ }
+
+ tmpl, _ := template.ParseFiles("templates/user/gallery.htm")
+ tmpl.Execute(&buf, tmp)
+ tmpl.ExecuteTemplate(&buf, "G", template.HTML(tmp.Posts))
+ tmpl.ExecuteTemplate(&buf, "P", template.HTML(tmp.Nav))
+ }
+
+ output <- buf.String()
+}
diff --git a/user/main.go b/user/main.go
new file mode 100644
index 0000000..2518176
--- /dev/null
+++ b/user/main.go
@@ -0,0 +1,22 @@
+package user
+
+import "strconv"
+
+func Get(name, action, page, query string) string {
+ c := make(chan string, 100)
+
+ switch action {
+ case "":
+ go about(&name, c)
+ case "gallery":
+ if page == "" {
+ page = "1"
+ }
+
+ go gallery(&name, c, page)
+ case "search":
+ p, _ := strconv.ParseInt(page, 10, 32)
+ go search(&name, &query, int(p), c)
+ }
+ return <-c
+}
diff --git a/user/misc.go b/user/misc.go
new file mode 100644
index 0000000..c424ad9
--- /dev/null
+++ b/user/misc.go
@@ -0,0 +1,46 @@
+package user
+
+import (
+ "skunkyart/util"
+)
+
+type ab struct {
+ ErrorDescription string
+ Owner struct {
+ Username string
+ }
+ Gruser struct {
+ Page struct {
+ Modules []struct {
+ Name string
+ ModuleData struct {
+ About struct {
+ Country, Website, WebsiteLabel, Gender, Tagline string
+ DeviantFor int64
+ SocialLinks []struct {
+ Value string
+ }
+ TextContent struct {
+ Excerpt string
+ Html struct {
+ Markup string
+ }
+ }
+ Interests []struct {
+ Label, Value string
+ }
+ }
+ CoverDeviation struct {
+ CoverDeviation util.Deviation
+ }
+ }
+ }
+ }
+ }
+ PageExtraData struct {
+ GruserTagline string
+ Stats struct {
+ Deviations, Watchers, Watching, Pageviews, CommentsReceivedProfile, CommentsMade, Favourites, Friends int
+ }
+ }
+}
diff --git a/user/search.go b/user/search.go
new file mode 100644
index 0000000..f555409
--- /dev/null
+++ b/user/search.go
@@ -0,0 +1,56 @@
+package user
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "html/template"
+ "math"
+ "skunkyart/util"
+ "strconv"
+)
+
+func search(user, query *string, page int, output chan string) {
+ var buf bytes.Buffer
+ {
+ var tmp struct {
+ Total int
+ Results, Pages, Name string
+ }
+
+ var q struct {
+ EstTotal int
+ Results []util.Deviation
+ }
+
+ if *query != "" {
+ rawjson := util.Puppy("dashared/gallection/search?username=" + *user + "&q=" + *query + "&offset=" + strconv.Itoa(50*page) + "&type=gallery&order=most-recent&init=true&limit=50")
+ err := json.Unmarshal([]byte(rawjson), &q)
+ if err != nil {
+ fmt.Println(err)
+ } else {
+ if q.EstTotal != 0 {
+ tmp.Total = q.EstTotal
+ }
+
+ for _, a := range q.Results {
+ tmp.Results += util.Images(a, a.Media, false)
+ }
+
+ tmp.Pages = ""
+ tmp.Name = *user
+ for x := 0; x < int(math.Round(float64(q.EstTotal/50))); x++ {
+ tmp.Pages += "" + strconv.Itoa(x+1) + " "
+ }
+ }
+ tmpl, _ := template.ParseFiles("templates/user/search.htm")
+ tmpl.Execute(&buf, tmp)
+ tmpl.ExecuteTemplate(&buf, "R", template.HTML(tmp.Results))
+ tmpl.ExecuteTemplate(&buf, "P", template.HTML(tmp.Pages))
+ } else {
+ tmpl, _ := template.ParseFiles("templates/user/search-base.htm")
+ tmpl.Execute(&buf, *user)
+ }
+ }
+ output <- buf.String()
+}
diff --git a/util/cache.go b/util/cache.go
new file mode 100644
index 0000000..6adfeee
--- /dev/null
+++ b/util/cache.go
@@ -0,0 +1,99 @@
+package util
+
+import (
+ "crypto/sha1"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+ "syscall"
+ "time"
+)
+
+func check(what ...string) {
+ if Conf.Cache.Max_size != 0 || Conf.Cache.Lifetime != 0 {
+ for {
+ dir, err := os.Open(Conf.Cache.Path)
+ if err != nil {
+ err = os.Mkdir(Conf.Cache.Path, 0700)
+ if err != nil {
+ panic(fmt.Sprintln(err))
+ }
+ }
+ stat, _ := dir.Stat()
+
+ c, _ := dir.Readdirnames(-1)
+ for _, a := range c {
+ for _, b := range what {
+ switch b {
+ case "lifetime":
+ if Conf.Cache.Lifetime != 0 {
+ now := time.Now().UnixMilli()
+
+ f, _ := os.Stat(Conf.Cache.Path + "/" + a)
+ stat := f.Sys().(*syscall.Stat_t)
+ time := time.Unix(stat.Ctim.Unix()).UnixMilli()
+
+ if time+Conf.Cache.Lifetime <= now {
+ os.RemoveAll(Conf.Cache.Path + "/" + a)
+ }
+ }
+ case "maxsize":
+ if Conf.Cache.Max_size != 0 && stat.Size() > Conf.Cache.Max_size {
+ os.RemoveAll(Conf.Cache.Path + "/" + a)
+ }
+ }
+ }
+ }
+
+ dir.Close()
+
+ time.Sleep(time.Second)
+ }
+ }
+}
+
+func Cache(img string) string {
+ if Conf.Cache.Enabled {
+ os.Mkdir(Conf.Cache.Path, 0700)
+
+ sum := sha1.Sum([]byte(img))
+ encsum := fmt.Sprintf("%X", sum[:])
+ file, err := os.Open(Conf.Cache.Path + "/" + encsum)
+
+ if err != nil {
+ body, status, _, h := Request(img, "")
+
+ if status == 200 {
+ for b, a := range h {
+ if b == "Content-Type" {
+ for _, a := range a {
+ a = a[0:strings.Index(a, "/")]
+ if a == "image" || a == "video" {
+ err := os.WriteFile(Conf.Cache.Path+"/"+encsum, []byte(body), 0700)
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ return string(body)
+ }
+ }
+ }
+ }
+ } else {
+ return "Failed to get image."
+ }
+ } else {
+ file, err := io.ReadAll(file)
+ if err != nil {
+ println(err)
+ }
+ return string(file)
+ }
+ file.Close()
+ } else {
+ body, _, _, _ := Request(img, "")
+ return string(body)
+ }
+ return ""
+}
diff --git a/util/config.go b/util/config.go
new file mode 100644
index 0000000..ed06010
--- /dev/null
+++ b/util/config.go
@@ -0,0 +1,76 @@
+package util
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "os"
+)
+
+type cconf struct {
+ Enabled bool
+ Path string
+ Max_size, Lifetime int64
+}
+
+type c struct {
+ Listen string
+ Cache cconf
+ Proxy, Nsfw bool
+}
+
+var Conf = c{
+ Listen: "127.0.0.1:3003",
+ Cache: cconf{
+ Enabled: true,
+ Path: "cache",
+ // Max_size: 4000000,
+ // Lifetime: 10000,
+ },
+ Proxy: true,
+ Nsfw: true,
+}
+
+func ParseConfig() {
+ cfile := "config.json"
+
+ for b, a := range os.Args {
+ if len(a) > 2 && a[0:2] == "--" {
+ a = a[2:]
+ switch a {
+ case "help":
+ println(`SkunkyArt v1.0 Beta
+Usage:
+ - config: specify config file path;
+ - help: prints this message.
+Example:
+ ./skunkyart --config /services/skunkyart/config.json
+
+lost+skunk, 2024. Source Code: https://git.macaw.me/skunky/skunkyart`)
+ os.Exit(0)
+ case "config":
+ file, err := os.ReadFile(os.Args[b+1])
+ if err != nil {
+ log.Fatalln(fmt.Sprint(err))
+ }
+
+ if len(file) > 0 {
+ if string(file)[0:1] == "{" {
+ cfile = os.Args[b+1]
+ } else {
+ log.Fatalln("Invalid config file.")
+ }
+ }
+ }
+ }
+ }
+
+ file, err := os.ReadFile(cfile)
+ if err != nil {
+ panic(fmt.Sprintln(err))
+ }
+
+ json.Unmarshal(file, &Conf)
+
+ go check("lifetime", "maxsize")
+}
diff --git a/util/dtype.go b/util/dtype.go
new file mode 100644
index 0000000..94397a9
--- /dev/null
+++ b/util/dtype.go
@@ -0,0 +1,115 @@
+package util
+
+import (
+ "strconv"
+ "strings"
+ "time"
+)
+
+type Deviation struct {
+ Title, Url, License string
+ PublishedTime Time
+ IsMature, IsAiGenerated, IsDailyDeviation bool
+ Author struct {
+ Username string
+ }
+ Stats struct {
+ Favourites, Views, Downloads int
+ }
+ Media Media
+ Extended struct {
+ Tags []struct {
+ Name string
+ }
+ DescriptionText struct {
+ Html HTML
+ }
+ RelatedContent []struct {
+ Deviations []Deviation
+ }
+ }
+ TextContent struct {
+ Html HTML
+ }
+}
+
+type Media struct {
+ BaseUri string
+ Token []string
+ Types []struct {
+ T string
+ H, W int
+ }
+}
+
+type HTML struct {
+ Markup, Type string
+}
+
+type Time struct {
+ time.Time
+}
+
+func (t *Time) UnmarshalJSON(b []byte) (err error) {
+ if b[0] == '"' && b[len(b)-1] == '"' {
+ b = b[1 : len(b)-1]
+ }
+ t.Time, err = time.Parse("2006-01-02T15:04:05-0700", string(b))
+ return
+}
+
+// обработка изображения(ий)
+func Images(deviation Deviation, media Media, nfmt bool) string {
+ var uri string
+ if len(media.BaseUri) > 0 {
+ uri = strings.Replace(media.BaseUri[21:], ".wixmp.com", "", 1)
+ }
+
+ var image string
+ for _, a := range media.Types {
+ if a.T == "fullview" {
+ image = uri
+ if len(media.Token) > 0 {
+ if media.BaseUri[len(media.BaseUri)-3:] == "gif" {
+ image = uri + "?t=" + media.Token[0]
+ } else {
+ image = uri + "/v1/fill/w_" + strconv.Itoa(a.W) + ",h_" + strconv.Itoa(a.H) + "/" + ".gif" + "?t=" + media.Token[0]
+ }
+ }
+ }
+ }
+
+ if nfmt {
+ return "/image/" + image
+ }
+
+ var link string
+ if deviation.Url != "" {
+ Url := deviation.Url[27:]
+ link = strings.ReplaceAll(Url, Url[0:strings.Index(Url, "/")], "")
+ }
+
+ if deviation.Author.Username != "" {
+ deviation.Author.Username = deviation.Author.Username + " — "
+ }
+
+ if image != "" {
+ image = ""
+ } else {
+ image = "
") + } + case html.TextToken: + buf.Write(tt.Text()) + } + } +} + +func tagval(t *html.Tokenizer) string { + for { + tt := t.Next() + switch tt { + case html.ErrorToken: + return "" + case html.TextToken: + return string(t.Text()) + } + } +} diff --git a/util/puppy.go b/util/puppy.go new file mode 100644 index 0000000..dff7bd2 --- /dev/null +++ b/util/puppy.go @@ -0,0 +1,48 @@ +package util + +import ( + "fmt" + "net/http" + "strconv" + "strings" +) + +var cookie []*http.Cookie +var zond, token string + +func CSRFToken() { + // получаем отслеживающий куки, без которого не удастся получить жсон в будущем + if cookie == nil || zond == "" { + _, _, _c, _ := Request("https://www.deviantart.com/_puppy/", "") + cookie = _c + + for _, a := range cookie { + zond = a.Raw + } + } + + body, status, _, _ := Request("https://www.deviantart.com/", zond) + if status != 200 { + token = "Something went wrong. Status: " + strconv.Itoa(status) + } + token = body[strings.Index(body, "window.__CSRF_TOKEN__ = '")+25 : strings.Index(body, "window.__XHR_LOCAL__")-3] +} + +func Puppy(data string) string { + // получаем жсон + if zond != "" { + if token[0:21] == "Something went wrong." { + return token + } + + jsonbody, status, _, _ := Request(fmt.Sprintf("https://www.deviantart.com/_puppy/%s&csrf_token=%s&da_minor_version=20230710", data, token), zond) + if status == 400 && jsonbody == `{"error":"invalid_request","errorDescription":"Invalid or expired form submission","errorDetails":{"csrf":"invalid"},"status":"error"}` { + cookie = nil + zond = "" + CSRFToken() + return jsonbody + } + return jsonbody + } + return "" +}