krz/devianter

A DeviantArt guest API library for Go.

clone: git clone https://gitbay.org/krz/devianter.git

66ab740d7fec430d97f69ab0ed52ba0f01973d46

unsigned

author: lost+skunk <skunky@macaw.me> · 2024-06-03T21:50:30Z

First Commit!
 .vscode/settings.json |   5 ++
 comments.go           |  79 ++++++++++++++++++++
 deviantion.go         | 117 +++++++++++++++++++++++++++++
 examples/post.go      |  25 +++++++
 go.mod                |   3 +
 implementation.md     |   4 +
 misc.go               | 198 ++++++++++++++++++++++++++++++++++++++++++++++++++
 todo.md               |   3 +
 8 files changed, 434 insertions(+)

diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..636b799
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+    "go.toolsEnvVars": {
+        "GOROOT": ""
+    }
+}
\ No newline at end of file
diff --git a/comments.go b/comments.go
new file mode 100644
index 0000000..f6a86c6
--- /dev/null
+++ b/comments.go
@@ -0,0 +1,79 @@
+package devianter
+
+import (
+	"encoding/json"
+	"strconv"
+	"strings"
+)
+
+type comments struct {
+	Cursor           string
+	PrevOffset       int
+	HasMore, HasLess bool
+
+	Total  int
+	Thread []struct {
+		CommentId, ParentId, Replies, Likes int
+		Posted                              time
+		IsAuthorHighlited                   bool
+
+		Desctiption string
+
+		// упрощение структуры с комментами
+		Comment string
+
+		TextContent struct {
+			Html struct {
+				Markup string
+			}
+		}
+
+		User struct {
+			Username string
+			IsBanned bool
+		}
+	}
+}
+
+// функция для обработки комментариев поста, пользователя, группы и многого другого
+func Comments(
+	postid string,
+	cursor string,
+	page int,
+	typ int, // 1 - комментарии поста; 4 - комментарии на стене группы или пользователя
+) (cmmts comments) {
+	for x := 0; x <= page; x++ {
+		ujson(
+			"dashared/comments/thread?typeid="+strconv.Itoa(typ)+
+				"&itemid="+postid+"&maxdepth=1000&order=newest"+
+				"&limit=50&cursor="+strings.ReplaceAll(cursor, "+", `%2B`),
+			&cmmts,
+		)
+
+		cursor = cmmts.Cursor
+
+		// парсинг json внутри json
+		for i := 0; i < len(cmmts.Thread); i++ {
+			m, l := cmmts.Thread[i].TextContent.Html.Markup, len(cmmts.Thread[i].TextContent.Html.Markup)
+			cmmts.Thread[i].Comment = m
+
+			// если начало строки {, а конец }, то срабатывает этот иф
+			if m[0] == 123 && m[l-1] == 125 {
+				var content struct {
+					Blocks []struct {
+						Text string
+					}
+				}
+
+				e := json.Unmarshal([]byte(m), &content)
+				err(e)
+
+				for _, a := range content.Blocks {
+					cmmts.Thread[i].Comment = a.Text
+				}
+			}
+		}
+	}
+
+	return
+}
diff --git a/deviantion.go b/deviantion.go
new file mode 100644
index 0000000..1f6644c
--- /dev/null
+++ b/deviantion.go
@@ -0,0 +1,117 @@
+package devianter
+
+import (
+	"encoding/json"
+	"strconv"
+	timelib "time"
+)
+
+// хрень для парсинга времени публикации
+type time struct {
+	timelib.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 = timelib.Parse("2006-01-02T15:04:05-0700", string(b))
+	return
+}
+
+// самая главная структура для поста
+type deviantion 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 text
+		RelatedContent  []struct {
+			Deviations []deviantion
+		}
+	}
+	TextContent text
+}
+
+// её выпердыши
+type media struct {
+	BaseUri string
+	Token   []string
+	Types   []struct {
+		T    string
+		H, W int
+	}
+}
+
+type text struct {
+	Html struct {
+		Markup, Type string
+	}
+}
+
+type Deviantion struct {
+	Deviation deviantion
+	Comments  struct {
+		Total  int
+		Cursor string
+	}
+
+	ParsedComments []struct {
+		Author         string
+		Posted         time
+		Replies, Likes int
+	}
+
+	IMG, Desctiption string
+	Recomendations   []deviantion
+}
+
+// для работы функции нужно ID поста и имя пользователя.
+func Deviation(id string, user string) Deviantion {
+	var st Deviantion
+	ujson(
+		"dadeviation/init?deviationid="+id+"&username="+user+"&type=art&include_session=false&expand=deviation.related&preload=true",
+		&st,
+	)
+
+	// преобразование урла в правильный
+	for _, t := range st.Deviation.Media.Types {
+		if m := st.Deviation.Media; t.T == "fullview" {
+			if len(m.Token) > 0 {
+				st.IMG = m.BaseUri + "?token="
+			} else {
+				st.IMG = m.BaseUri + "/v1/fill/w_" + strconv.Itoa(t.W) + ",h_" + strconv.Itoa(t.H) + "/" + id + "_" + user + ".gif" + "?token="
+			}
+			st.IMG += m.Token[0]
+		}
+	}
+
+	// базовая обработка описания
+	txt := st.Deviation.TextContent.Html.Markup
+	if len(txt) > 0 && txt[0:1] == "{" {
+		var description struct {
+			Blocks []struct {
+				Text string
+			}
+		}
+
+		json.Unmarshal([]byte(txt), &description)
+		for _, a := range description.Blocks {
+			txt = a.Text
+		}
+	}
+
+	st.Desctiption = txt
+
+	return st
+}
diff --git a/examples/post.go b/examples/post.go
new file mode 100644
index 0000000..2789c46
--- /dev/null
+++ b/examples/post.go
@@ -0,0 +1,25 @@
+package main
+
+import (
+	"git.macaw.me/skunky/devianter"
+)
+
+func main() {
+	id := "973578309"
+	d := devianter.Deviation(id, "Thrumyeye")
+
+	println("Post Name:", d.Deviation.Title, "\nIMG url:", d.IMG)
+
+	c := devianter.Comments(id, "", 0, 1)
+	println("\n\nPost Comments:", c.Total)
+
+	for _, a := range c.Thread {
+		if a.User.IsBanned {
+			a.User.Username += " [v bane]"
+		}
+		println(a.User.Username+":", a.Comment)
+	}
+
+	search := devianter.Search("skunk", "2", 'a')
+	println(search.Total, search.Pages)
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..c17f7e8
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module git.macaw.me/skunky/devianter
+
+go 1.18
diff --git a/implementation.md b/implementation.md
new file mode 100644
index 0000000..3f1c58a
--- /dev/null
+++ b/implementation.md
@@ -0,0 +1,4 @@
+- avatars
+- emojis
+- deviantions
+- comments
diff --git a/misc.go b/misc.go
new file mode 100644
index 0000000..c847e44
--- /dev/null
+++ b/misc.go
@@ -0,0 +1,198 @@
+package devianter
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"log"
+	"math"
+	"net/http"
+	"strings"
+)
+
+// функция для высера ошибки в stderr
+func err(txt error) {
+	if txt != nil {
+		println(txt.Error())
+	}
+}
+
+// сокращение для вызова щенка и парсинга жсона
+func ujson(data string, output any) {
+	input, e := puppy(data)
+	err(e)
+
+	eee := json.Unmarshal([]byte(input), output)
+	err(eee)
+}
+
+/* REQUEST SECTION */
+// структура для ответа сервера
+type reqrt struct {
+	Body    string
+	Status  int
+	Cookies []*http.Cookie
+	Headers http.Header
+}
+
+// функция для совершения запроса
+func request(uri string, other ...string) reqrt {
+	var r reqrt
+
+	// создаём новый запрос
+	cli := &http.Client{}
+	req, e := http.NewRequest("GET", uri, nil)
+	err(e)
+
+	req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0.0")
+
+	// куки и UA-шник
+	if other != nil {
+		for num, rng := range other {
+			switch num {
+			case 1:
+				req.Header.Set("User-Agent", rng)
+			case 0:
+				req.Header.Set("Cookie", rng)
+			}
+		}
+	}
+
+	resp, e := cli.Do(req)
+	err(e)
+	defer resp.Body.Close()
+
+	body, e := io.ReadAll(resp.Body)
+	err(e)
+
+	// заполняем структуру
+	r.Body = string(body)
+	r.Cookies = resp.Cookies()
+	r.Headers = resp.Header
+	r.Status = resp.StatusCode
+
+	return r
+}
+
+/* AVATARS AND EMOJIS */
+func AEmedia(name string, t rune) (string, error) {
+	// список всех возможных расширений
+	var extensions = [3]string{
+		".jpg",
+		".png",
+		".gif",
+	}
+	// надо
+	name = strings.ToLower(name)
+
+	// построение ссылок. билдер потому что он быстрее обычного сложения строк.
+	var b strings.Builder
+	switch t {
+	case 'a':
+		b.WriteString("https://a.deviantart.net/avatars-big/")
+		b.WriteString(name[:1])
+		b.WriteString("/")
+		b.WriteString(name[1:2])
+		b.WriteString("/")
+	case 'e':
+		b.WriteString("https://e.deviantart.net/emoticons/")
+		b.WriteString(name[:1])
+		b.WriteString("/")
+	default:
+		log.Fatalln("Invalid type.\n- 'a' -- avatar;\n- 'e' -- emoji.")
+	}
+	b.WriteString(name)
+
+	// проверка ссылки на доступность
+	for x := 0; x < len(extensions); x++ {
+		req := request(b.String() + extensions[x])
+		if req.Status == 200 {
+			return req.Body, nil
+		}
+	}
+
+	return "", errors.New("User not exists")
+}
+
+/* SEARCH */
+type search struct {
+	Total      int `json:"estTotal"`
+	Pages      int // only for 'a' scope.
+	Deviations []deviantion
+}
+
+func Search(query, page string, scope rune) (ss search) {
+	var url strings.Builder
+
+	// о5 построение ссылок.
+	switch scope {
+	case 'a':
+		url.WriteString("dabrowse/search/all?q=")
+	case 't':
+		url.WriteString("dabrowse/networkbar/tag/deviations?tag=")
+	default:
+		log.Fatalln("Invalid type.\n- 'a' -- all;\n- 't' -- tag.")
+	}
+
+	url.WriteString(query)
+	url.WriteString("&page=")
+	url.WriteString(page)
+
+	ujson(url.String(), &ss)
+
+	// расчёт, сколько всего страниц по запросу. без токена 417 страниц - максимум
+	for x := 0; x < int(math.Round(float64(ss.Total/25))); x++ {
+		if x <= 417 {
+			ss.Pages = x
+		}
+	}
+
+	return
+}
+
+/* PUPPY aka DeviantArt API */
+func puppy(data string) (string, error) {
+	// получение или обновление токена
+	update := func() (string, string, error) {
+		var cookie string
+		if cookie == "" {
+			req := request("https://www.deviantart.com/_puppy")
+
+			for _, content := range req.Cookies {
+				cookie = content.Raw
+			}
+		}
+
+		req := request("https://www.deviantart.com", cookie)
+		if req.Status != 200 {
+			return "", "", errors.New(req.Body)
+		}
+
+		return cookie, req.Body[strings.Index(req.Body, "window.__CSRF_TOKEN__ = '")+25 : strings.Index(req.Body, "window.__XHR_LOCAL__")-3], nil
+	}
+
+	// использование токена
+	var (
+		cookie, token string
+	)
+	if cookie == "" || token == "" {
+		var e error
+		cookie, token, e = update()
+		if e != nil {
+			return "", e
+		}
+	}
+
+	body := request(
+		fmt.Sprintf("https://www.deviantart.com/_puppy/%s&csrf_token=%s&da_minor_version=20230710", data, token),
+		cookie,
+	)
+
+	// если код ответа не 200, возвращается ошибка
+	if body.Status != 200 {
+		return "", errors.New(body.Body)
+	}
+
+	return body.Body, nil
+}
diff --git a/todo.md b/todo.md
new file mode 100644
index 0000000..0648437
--- /dev/null
+++ b/todo.md
@@ -0,0 +1,3 @@
+- ~~users~~
+- ~~groups~~
+- ~~images in comments~~
\ No newline at end of file