First commit!
This commit is contained in:
commit
883b4880e5
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"listen": "0.0.0.0:3003",
|
||||
"cache": {
|
||||
"enabled": true,
|
||||
"path": "cache",
|
||||
"lifetime": null,
|
||||
"max_size": 10000
|
||||
},
|
||||
"proxy": false,
|
||||
"nsfw": false
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
module skunkyart
|
||||
|
||||
go 1.18
|
||||
|
||||
require golang.org/x/net v0.21.0
|
|
@ -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=
|
|
@ -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:]
|
||||
}
|
|
@ -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 += "<a href=\"/dd?p=" + strconv.Itoa(int(page)-1) + "\"><-- Back</a> "
|
||||
}
|
||||
p += "<a href=\"/dd?p=" + strconv.Itoa(int(page)+1) + "\">Next --></a>"
|
||||
|
||||
tmp, _ := t.ParseFiles("templates/dd.htm")
|
||||
tmp.Execute(&buf, t.HTML(d))
|
||||
tmp.ExecuteTemplate(&buf, "P", t.HTML(p))
|
||||
}
|
||||
out <- buf.String()
|
||||
}
|
|
@ -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."
|
||||
}
|
|
@ -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 += "<a href=\"/search?p=" + strconv.Itoa(x+1) + "&scope=all&q=" + q + "\">" + strconv.Itoa(x+1) + "</a> "
|
||||
}
|
||||
case "tag":
|
||||
if p == "" {
|
||||
p = "1"
|
||||
}
|
||||
next, _ := strconv.ParseInt(p, 10, 32)
|
||||
if int(next) > 1 {
|
||||
tmp.Pages += "<a href=\"/search?p=" + strconv.Itoa(int(next)-1) + "&scope=tag&q=" + q + "\"><-- Back</a> "
|
||||
}
|
||||
tmp.Pages += "<a href=\"/search?p=" + strconv.Itoa(int(next)+1) + "&scope=tag&q=" + q + "\">Next --></a>"
|
||||
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()
|
||||
}
|
|
@ -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("<img src=\"" + a.Data.Src + "\">" + a.Data.Title + "</img>")
|
||||
// msgbody += "<img src=\"" + a.Data.Src + "\">" + a.Data.Title + "</img>"
|
||||
// }
|
||||
}
|
||||
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 = "<b>In reply to <a href=\"#" + strconv.Itoa(b.ParentId) + "\">message</a></b>:"
|
||||
}
|
||||
|
||||
msgbody = util.Parse(msgbody)
|
||||
|
||||
htm += fmt.Sprintf("<div class=\"msg\" style=\"%[10]s\"><p id=\"%[2]d\"><img src=\"/avatar/%[1]s\" width=\"30px\" height=\"30px\"/> <a href=\"/user/%[1]s\"><b class=\"%[6]s %[5]t\">%[1]s</b></a> %[4]s %[9]s<p>%[3]s<p>👍: %[7]d ⏩: %[8]d</p></div>\n", b.User.Username, b.CommentId, msgbody, b.Posted.UTC().String(), b.User.IsBanned, isauthor, b.Likes, b.Replies, reply, space)
|
||||
}
|
||||
|
||||
// если комментариев больше, чем 0, то отображаем их, иначе пишем, что комментов нет.
|
||||
if comments.Total > 0 {
|
||||
htm = "<h2 id=\"comments\">Comments (" + strconv.Itoa(comments.Total) + ")</h2>" + htm
|
||||
uri = uri[strings.LastIndex(uri, "/")+1:]
|
||||
if prev {
|
||||
htm += "<a href=\"" + uri + "?page=" + strconv.Itoa(page-2) + "#comments\"><-- Back</a> "
|
||||
}
|
||||
if next {
|
||||
htm += "<a href=\"" + uri + "?page=" + strconv.Itoa(page) + "#comments\">Next --></a>"
|
||||
}
|
||||
} else {
|
||||
htm = "<p>There is no comments.</p>"
|
||||
}
|
||||
|
||||
return &htm
|
||||
}
|
|
@ -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 + "<br>"
|
||||
} else {
|
||||
tmp.Description = a.Text
|
||||
}
|
||||
}
|
||||
}
|
||||
tmp.Description = util.Parse(tmp.Description)
|
||||
}
|
||||
|
||||
tmp.Downloads = j.Deviation.Stats.Downloads
|
||||
tmp.Ai = j.Deviation.IsAiGenerated
|
||||
tmp.License = j.Deviation.License
|
||||
tmp.DD = j.Deviation.IsDailyDeviation
|
||||
|
||||
tmp.Recomendations = ""
|
||||
for _, a := range j.Deviation.Extended.RelatedContent {
|
||||
for _, a := range a.Deviations {
|
||||
tmp.Recomendations += util.Images(a, a.Media, false)
|
||||
}
|
||||
}
|
||||
|
||||
tmp.Tags = ""
|
||||
for _, a := range j.Deviation.Extended.Tags {
|
||||
tmp.Tags += "<a href=\"/search?q=" + a.Name + "&scope=tag\">#" + a.Name + "</a> "
|
||||
}
|
||||
|
||||
tmp.Comments = ""
|
||||
if j.Comments.Total > 0 {
|
||||
tmp.Comments = *comments(url, tmp.Author, page, strconv.Itoa(j.Comments.Cursor), id)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func Get(uri string, page int, output chan string) {
|
||||
var url string
|
||||
|
||||
switch {
|
||||
case strings.Contains(uri, "journal/"):
|
||||
url = "https://www.deviantart.com/" + uri
|
||||
default:
|
||||
url = "https://www.deviantart.com/art/" + uri
|
||||
}
|
||||
|
||||
id := regexp.MustCompile("[0-9]+").FindAllString(uri, -1)
|
||||
|
||||
var buffer bytes.Buffer
|
||||
body, status, _, _ := util.Request(url, "")
|
||||
if status == 200 {
|
||||
body = body[strings.Index(body, " by ")+4:]
|
||||
err := _post(id, body[0:strings.Index(body, " ")], uri, page)
|
||||
|
||||
if err != "" {
|
||||
output <- err
|
||||
}
|
||||
|
||||
// темплейт
|
||||
templ, _ := template.ParseFiles("templates/post.htm")
|
||||
templ.Execute(&buffer, tmp)
|
||||
templ.ExecuteTemplate(&buffer, "T", template.HTML(tmp.Tags))
|
||||
templ.ExecuteTemplate(&buffer, "D", template.HTML(tmp.Description))
|
||||
templ.ExecuteTemplate(&buffer, "R", template.HTML(tmp.Recomendations))
|
||||
templ.ExecuteTemplate(&buffer, "C", template.HTML(string(tmp.Comments)))
|
||||
|
||||
output <- buffer.String()
|
||||
} else {
|
||||
output <- "Something went wrong. Status: " + strconv.Itoa(int(status))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
html {
|
||||
font-family: ubuntu;
|
||||
background-color:black;
|
||||
color: whitesmoke;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: cadetblue;
|
||||
}
|
||||
form input, button, select {
|
||||
background-color: #134134;
|
||||
padding: 5px;
|
||||
color: whitesmoke;
|
||||
border: 0px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.nsfw, .true {
|
||||
color: red;
|
||||
}
|
||||
.author {
|
||||
color: seagreen;
|
||||
}
|
||||
.msg {
|
||||
background-color: #091f19;
|
||||
color: whitesmoke;
|
||||
width: fit-content;
|
||||
max-width: 90%;
|
||||
padding: 4px;
|
||||
border-radius: 2px;
|
||||
margin-top: 6px;
|
||||
text-wrap: pretty;
|
||||
transition: 350ms;
|
||||
}
|
||||
.msg:hover {
|
||||
background-color: #134134;
|
||||
}
|
||||
.dd {
|
||||
color: rgb(160, 0, 147);
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
.block {
|
||||
max-width: 20%;
|
||||
height: 0%;
|
||||
padding: 4px;
|
||||
border-radius: 2px;
|
||||
border: 3px solid #091f19;
|
||||
word-break: break-all;
|
||||
background-color: #091f19;
|
||||
margin-left: 5px;
|
||||
margin-top: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
.block:hover {
|
||||
border: 3px solid #4d27d6;
|
||||
transition: 400ms;
|
||||
}
|
||||
.block img {
|
||||
width: 100%;
|
||||
}
|
||||
.block p {
|
||||
word-break: break-all;
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Daily Deviations | SkunkyArt</title>
|
||||
<link rel="stylesheet" href="/static/base.css">
|
||||
</head>
|
||||
<body>
|
||||
<form method="get" action="/search">
|
||||
<input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
|
||||
<select name="scope">
|
||||
<option value="all">All</option>
|
||||
<option value="tag">Tag</option>
|
||||
</select>
|
||||
<button type="submit">Search!</button>
|
||||
</form>
|
||||
<h1>Daily Deviations</h1>
|
||||
<div class="content">
|
||||
{{.}}
|
||||
</div>
|
||||
{{define "P"}}
|
||||
<hr>
|
||||
{{.}}
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
|
@ -0,0 +1,19 @@
|
|||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>SkunkyArt</title>
|
||||
<link rel="stylesheet" href="/static/base.css">
|
||||
</head>
|
||||
<body>
|
||||
<form method="get" action="/search">
|
||||
<input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
|
||||
<select name="scope">
|
||||
<option value="all">All</option>
|
||||
<option value="tag">Tag</option>
|
||||
</select>
|
||||
<button type="submit">Search!</button>
|
||||
</form>
|
||||
<h1><a href="/dd">Daily Deviations</a> <a href="/about">About</a></h1>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,53 @@
|
|||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{.Author}} — {{.Postname}} | SkunkyArt</title>
|
||||
<link rel="stylesheet" href="/static/base.css">
|
||||
</head>
|
||||
<body>
|
||||
<form method="get" action="/search">
|
||||
<input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
|
||||
<select name="scope">
|
||||
<option value="all">All</option>
|
||||
<option value="tag">Tag</option>
|
||||
</select>
|
||||
<button type="submit">Search!</button>
|
||||
</form>
|
||||
<figure>
|
||||
<img src="/avatar/{{.Author}}" width="30px">
|
||||
<span><strong><a href="/user/{{.Author}}">{{.Author}}</a></strong> — {{if (.DD)}}
|
||||
<span class="dd" title="Daily Deviation!"><b>{{.Postname}}</b></span>
|
||||
{{else}}{{.Postname}}{{end}}
|
||||
{{if (ne .License "none")}}({{.License}}){{end}} {{if (.Ai)}}[🤖]{{end}}
|
||||
{{if (.Nsfw)}}[<span class="nsfw">NSFW</span>]{{end}}
|
||||
</span>
|
||||
<br>
|
||||
{{if (ne .Img "/image/")}}
|
||||
<img src="{{.Img}}" width="50%">
|
||||
<br>
|
||||
{{end}}
|
||||
<span>Published: <strong>{{.Time}}</strong>; Views: <strong>{{.Views}}</strong>; Favourites: <strong>{{.FavsCount}}</strong>; Downloads: <strong>{{.Downloads}}</strong></span>
|
||||
{{define "T"}}
|
||||
{{if (ne . "")}}
|
||||
<br>
|
||||
<span>Tags: <strong>{{.}}</strong></span>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{define "R"}}
|
||||
<h2>See also:</h2>
|
||||
<div class="content">
|
||||
{{.}}
|
||||
</div>
|
||||
{{end}}
|
||||
<br>
|
||||
<br>
|
||||
{{define "D"}}
|
||||
<figcaption>{{.}}</figcaption>
|
||||
{{end}}
|
||||
{{define "C"}}
|
||||
{{.}}
|
||||
</figure>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
|
@ -0,0 +1,18 @@
|
|||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Search | SkunkyArt</title>
|
||||
<link rel="stylesheet" href="/static/base.css">
|
||||
</head>
|
||||
<body>
|
||||
<form method="get" action="/search">
|
||||
<input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
|
||||
<select name="scope">
|
||||
<option value="all">All</option>
|
||||
<option value="tag">Tag</option>
|
||||
</select>
|
||||
<button type="submit">Search!</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,31 @@
|
|||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Search | SkunkyArt</title>
|
||||
<link rel="stylesheet" href="/static/base.css">
|
||||
</head>
|
||||
<body>
|
||||
<form method="get" action="/search">
|
||||
<input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
|
||||
<select name="scope">
|
||||
<option value="all">All</option>
|
||||
<option value="tag">Tag</option>
|
||||
</select>
|
||||
<button type="submit">Search!</button>
|
||||
</form>
|
||||
<hr>
|
||||
{{if (ne . 0)}}
|
||||
<h1>Total resuls: {{.}}</h1>
|
||||
{{end}}
|
||||
{{define "R"}}
|
||||
<div class="content">
|
||||
{{.}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{define "P"}}
|
||||
<hr>
|
||||
{{.}}
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
|
||||
<html>
|
||||
<head>
|
||||
<title>{{.Name}}'s Gallery | SkunkyArt</title>
|
||||
<link rel="stylesheet" href="/static/base.css">
|
||||
</head>
|
||||
<body>
|
||||
<img src="/avatar/{{.Name}}" width="60px"><h1>Gallery of {{.Name}}</h1></img>
|
||||
<h2><a href="/user/{{.Name}}">Home</a> | <a href="/user/{{.Name}}?a=search">Search</a></h2>
|
||||
{{define "G"}}
|
||||
<div class="content">
|
||||
{{.}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{define "P"}}
|
||||
<hr>
|
||||
<p><b>Pages: {{.}}</b></p>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
|
@ -0,0 +1,36 @@
|
|||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
|
||||
<html>
|
||||
<head>
|
||||
<title>{{.Name}} | SkunkyArt</title>
|
||||
<link rel="stylesheet" href="/static/base.css">
|
||||
</head>
|
||||
<body>
|
||||
{{if (ne .Bg "/image/")}}
|
||||
<img src="{{.Bg}}" width="30%" title="{{.Bgname}}">
|
||||
{{end}}
|
||||
<h1><img src="/avatar/{{.Name}}" width="60px">{{.Name}} {{.Gender}}</h1>
|
||||
<h2><a href="/user/{{.Name}}?a=gallery">Gallery</a></h2>
|
||||
<p>Watchers: {{.Watchers}}; Watching: {{.Watching}}; Comments on Profile: {{.CMMRCPF}}; Posts: {{.Posts}}; Pageviews: {{.Pageviews}}; Comments: {{.CM}};
|
||||
Favourites: {{.Fav}}; Friends: {{.Friends}}
|
||||
</p>
|
||||
<p>Registration Date: <b>{{.RegDate}}</b>
|
||||
<p>From <b>{{.Country}}</b>.<p>
|
||||
<a href="https://{{.Site}}" target="_blank">{{.SiteTitle}}</a>
|
||||
</p>
|
||||
<p><i title="User status">{{.Status}}</i></p>
|
||||
{{define "S"}}
|
||||
<h2>Social Links</h2>
|
||||
<p>
|
||||
{{.}}
|
||||
</p>
|
||||
{{end}}
|
||||
{{define "I"}}
|
||||
<h2>Interests</h2>
|
||||
<p>{{.}}</p>
|
||||
{{end}}
|
||||
{{define "D"}}
|
||||
<h2>About me</h2>
|
||||
<p>{{.}}</p>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
|
@ -0,0 +1,16 @@
|
|||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
|
||||
<html>
|
||||
<head>
|
||||
<title>Search {{.}}'s Gallery | SkunkyArt</title>
|
||||
<link rel="stylesheet" href="/static/base.css">
|
||||
</head>
|
||||
<body>
|
||||
<img src="/avatar/{{.}}" width="60px"><h1>Search gallery of {{.}}.</h1></img>
|
||||
<h2><a href="/user/{{.}}">Home</a> | <a href="/user/{{.}}?a=gallery">Gallery</a></h2>
|
||||
<form method="get" action="/user/{{.}}">
|
||||
<input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
|
||||
<input type="hidden" name="a" value="search">
|
||||
<button type="submit">Search!</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,25 @@
|
|||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
|
||||
<html>
|
||||
<head>
|
||||
<title>Search {{.Name}}'s Gallery | SkunkyArt</title>
|
||||
<link rel="stylesheet" href="/static/base.css">
|
||||
</head>
|
||||
<body>
|
||||
<img src="/avatar/{{.Name}}" width="60px"><h1>Search gallery of {{.Name}}. Total Deviations: {{.Total}}</h1></img>
|
||||
<h2><a href="/user/{{.Name}}">Home</a> | <a href="/user/{{.Name}}?a=gallery">Gallery</a></h2>
|
||||
<form method="get" action="/user/{{.Name}}">
|
||||
<input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
|
||||
<input type="hidden" name="a" value="search">
|
||||
<button type="submit">Search!</button>
|
||||
</form>
|
||||
{{define "R"}}
|
||||
<div class="content">
|
||||
{{.}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{define "P"}}
|
||||
<hr>
|
||||
<p><b>Pages: {{.}}</b></p>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
|
@ -0,0 +1,12 @@
|
|||
Нужно сделать:
|
||||
- провести оптимизацию кода
|
||||
- кеширование жсона
|
||||
- ~~добавить устаревание кеша и его максимальный вес~~
|
||||
- ~~проксирование и кеширование гифок~~
|
||||
- картинки в комментариях
|
||||
- улучшить парсинг HTML
|
||||
- улучшить парсинг описания профиля пользователя
|
||||
- реализовать RSS-ленты пользователей
|
||||
- ~~добавить возможность включать/выключать разрешение просматривать NSFW-контент в конфиге~~
|
||||
- реализовать систему аккаунтов
|
||||
- написать несколько тем, в том числе, легковесных
|
|
@ -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 + "<p>"
|
||||
}
|
||||
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 href=\"" + a.Value + "\" target=\"_blank\">" + a.Value + "</a><p>"
|
||||
}
|
||||
|
||||
for _, a := range a.ModuleData.About.Interests {
|
||||
tmp.Interests += "<b>" + a.Label + "</b>: " + a.Value + "<p>"
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
|
@ -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 += "<a href=\"/user/" + tmp.Name + "?a=gallery&p=" + strconv.Itoa(x+1) + "\">" + strconv.Itoa(x+1) + "</a> "
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 += "<a href=\"/user/" + tmp.Name + "?a=search&p=" + strconv.Itoa(x+1) + "&q=" + *query + "\">" + strconv.Itoa(x+1) + "</a> "
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
|
@ -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 ""
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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 = "<img src=\"/image/" + image + "\" width=\"15%\">"
|
||||
} else {
|
||||
image = "<h1>[ TEXT ]</h1>"
|
||||
}
|
||||
|
||||
switch {
|
||||
case deviation.IsAiGenerated:
|
||||
deviation.Title += " [🤖]"
|
||||
case deviation.IsDailyDeviation:
|
||||
deviation.Title += " [<span class=\"dd\">DD</span>]"
|
||||
case deviation.IsMature:
|
||||
if !Conf.Nsfw {
|
||||
return ""
|
||||
}
|
||||
deviation.Title += " [<span class=\"nsfw\">NSFW</span>]"
|
||||
}
|
||||
|
||||
return "<div class=\"block\">" + image + "<br><a href=\"/post" + link + "\">" + deviation.Author.Username + deviation.Title + "</a></div>"
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
func Request(url string, cookie string) (string, int, []*http.Cookie, http.Header) {
|
||||
// println(url)
|
||||
cli := &http.Client{}
|
||||
request, err := http.NewRequest("GET", url, nil)
|
||||
request.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0.0")
|
||||
request.Header.Set("Cookie", cookie)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
response, err := cli.Do(request)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
response.Body.Close()
|
||||
return string(body), response.StatusCode, response.Cookies(), response.Header
|
||||
}
|
||||
|
||||
func Render(n *html.Node) string {
|
||||
var buffer bytes.Buffer
|
||||
html.Render(io.Writer(&buffer), n)
|
||||
return buffer.String()
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
func Parse(n string) string {
|
||||
var buf bytes.Buffer
|
||||
tt := html.NewTokenizer(strings.NewReader(n))
|
||||
for {
|
||||
t := tt.Next()
|
||||
switch t {
|
||||
case html.ErrorToken:
|
||||
return buf.String()
|
||||
case html.StartTagToken, html.EndTagToken, html.SelfClosingTagToken:
|
||||
token := tt.Token()
|
||||
switch token.Data {
|
||||
case "a":
|
||||
for _, a := range token.Attr {
|
||||
if a.Key == "href" {
|
||||
url := strings.ReplaceAll(a.Val, "https://www.deviantart.com/users/outgoing?", "")
|
||||
if strings.Contains(url, "deviantart") {
|
||||
url = strings.ReplaceAll(url, "https://www.deviantart.com/", "")
|
||||
url = strings.ReplaceAll(url, url[0:strings.Index(url, "/")+1], "")
|
||||
}
|
||||
buf.WriteString("<a target=\"_blank\" href=\"" + url + "\">" + tagval(tt) + "</a> ")
|
||||
}
|
||||
}
|
||||
case "img":
|
||||
var (
|
||||
uri, title string
|
||||
)
|
||||
for b, a := range token.Attr {
|
||||
switch a.Key {
|
||||
case "src":
|
||||
if a.Val[8:9] == "e" {
|
||||
uri = "/emoji/" + a.Val[37:len(a.Val)-4]
|
||||
}
|
||||
case "title":
|
||||
title = a.Val
|
||||
}
|
||||
if title != "" {
|
||||
for x := -1; x < b; x++ {
|
||||
buf.WriteString("<img src=\"" + uri + "\" title=\"" + title + "\">")
|
||||
}
|
||||
}
|
||||
}
|
||||
case "br", "li", "ul", "p", "b":
|
||||
buf.WriteString(token.String())
|
||||
case "div":
|
||||
buf.WriteString("<p> ")
|
||||
}
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 ""
|
||||
}
|
Loading…
Reference in New Issue