Commit 66ab740d7f

66ab740d7fec430d97f69ab0ed52ba0f01973d46

parent: 3334c33e04

Unsigned

lost+skunk <skunky@macaw.me> · 2024-06-03 21:50 UTC

First Commit!

Layout: unified · split

.vscode/settings.json added +5
@@ -0,0 +1,5 @@
1{
2 "go.toolsEnvVars": {
3 "GOROOT": ""
4 }
5}\ No newline at end of file
comments.go added +79
@@ -0,0 +1,79 @@
1package devianter
2
3import (
4 "encoding/json"
5 "strconv"
6 "strings"
7)
8
9type comments struct {
10 Cursor string
11 PrevOffset int
12 HasMore, HasLess bool
13
14 Total int
15 Thread []struct {
16 CommentId, ParentId, Replies, Likes int
17 Posted time
18 IsAuthorHighlited bool
19
20 Desctiption string
21
22 // упрощение структуры с комментами
23 Comment string
24
25 TextContent struct {
26 Html struct {
27 Markup string
28 }
29 }
30
31 User struct {
32 Username string
33 IsBanned bool
34 }
35 }
36}
37
38// функция для обработки комментариев поста, пользователя, группы и многого другого
39func Comments(
40 postid string,
41 cursor string,
42 page int,
43 typ int, // 1 - комментарии поста; 4 - комментарии на стене группы или пользователя
44) (cmmts comments) {
45 for x := 0; x <= page; x++ {
46 ujson(
47 "dashared/comments/thread?typeid="+strconv.Itoa(typ)+
48 "&itemid="+postid+"&maxdepth=1000&order=newest"+
49 "&limit=50&cursor="+strings.ReplaceAll(cursor, "+", `%2B`),
50 &cmmts,
51 )
52
53 cursor = cmmts.Cursor
54
55 // парсинг json внутри json
56 for i := 0; i < len(cmmts.Thread); i++ {
57 m, l := cmmts.Thread[i].TextContent.Html.Markup, len(cmmts.Thread[i].TextContent.Html.Markup)
58 cmmts.Thread[i].Comment = m
59
60 // если начало строки {, а конец }, то срабатывает этот иф
61 if m[0] == 123 && m[l-1] == 125 {
62 var content struct {
63 Blocks []struct {
64 Text string
65 }
66 }
67
68 e := json.Unmarshal([]byte(m), &content)
69 err(e)
70
71 for _, a := range content.Blocks {
72 cmmts.Thread[i].Comment = a.Text
73 }
74 }
75 }
76 }
77
78 return
79}
deviantion.go added +117
@@ -0,0 +1,117 @@
1package devianter
2
3import (
4 "encoding/json"
5 "strconv"
6 timelib "time"
7)
8
9// хрень для парсинга времени публикации
10type time struct {
11 timelib.Time
12}
13
14func (t *time) UnmarshalJSON(b []byte) (err error) {
15 if b[0] == '"' && b[len(b)-1] == '"' {
16 b = b[1 : len(b)-1]
17 }
18 t.Time, err = timelib.Parse("2006-01-02T15:04:05-0700", string(b))
19 return
20}
21
22// самая главная структура для поста
23type deviantion struct {
24 Title, Url, License string
25 PublishedTime time
26 IsMature, IsAiGenerated, IsDailyDeviation bool
27 Author struct {
28 Username string
29 }
30 Stats struct {
31 Favourites, Views, Downloads int
32 }
33 Media media
34 Extended struct {
35 Tags []struct {
36 Name string
37 }
38 DescriptionText text
39 RelatedContent []struct {
40 Deviations []deviantion
41 }
42 }
43 TextContent text
44}
45
46// её выпердыши
47type media struct {
48 BaseUri string
49 Token []string
50 Types []struct {
51 T string
52 H, W int
53 }
54}
55
56type text struct {
57 Html struct {
58 Markup, Type string
59 }
60}
61
62type Deviantion struct {
63 Deviation deviantion
64 Comments struct {
65 Total int
66 Cursor string
67 }
68
69 ParsedComments []struct {
70 Author string
71 Posted time
72 Replies, Likes int
73 }
74
75 IMG, Desctiption string
76 Recomendations []deviantion
77}
78
79// для работы функции нужно ID поста и имя пользователя.
80func Deviation(id string, user string) Deviantion {
81 var st Deviantion
82 ujson(
83 "dadeviation/init?deviationid="+id+"&username="+user+"&type=art&include_session=false&expand=deviation.related&preload=true",
84 &st,
85 )
86
87 // преобразование урла в правильный
88 for _, t := range st.Deviation.Media.Types {
89 if m := st.Deviation.Media; t.T == "fullview" {
90 if len(m.Token) > 0 {
91 st.IMG = m.BaseUri + "?token="
92 } else {
93 st.IMG = m.BaseUri + "/v1/fill/w_" + strconv.Itoa(t.W) + ",h_" + strconv.Itoa(t.H) + "/" + id + "_" + user + ".gif" + "?token="
94 }
95 st.IMG += m.Token[0]
96 }
97 }
98
99 // базовая обработка описания
100 txt := st.Deviation.TextContent.Html.Markup
101 if len(txt) > 0 && txt[0:1] == "{" {
102 var description struct {
103 Blocks []struct {
104 Text string
105 }
106 }
107
108 json.Unmarshal([]byte(txt), &description)
109 for _, a := range description.Blocks {
110 txt = a.Text
111 }
112 }
113
114 st.Desctiption = txt
115
116 return st
117}
examples/post.go added +25
@@ -0,0 +1,25 @@
1package main
2
3import (
4 "git.macaw.me/skunky/devianter"
5)
6
7func main() {
8 id := "973578309"
9 d := devianter.Deviation(id, "Thrumyeye")
10
11 println("Post Name:", d.Deviation.Title, "\nIMG url:", d.IMG)
12
13 c := devianter.Comments(id, "", 0, 1)
14 println("\n\nPost Comments:", c.Total)
15
16 for _, a := range c.Thread {
17 if a.User.IsBanned {
18 a.User.Username += " [v bane]"
19 }
20 println(a.User.Username+":", a.Comment)
21 }
22
23 search := devianter.Search("skunk", "2", 'a')
24 println(search.Total, search.Pages)
25}
go.mod added +3
@@ -0,0 +1,3 @@
1module git.macaw.me/skunky/devianter
2
3go 1.18
implementation.md added +4
@@ -0,0 +1,4 @@
1- avatars
2- emojis
3- deviantions
4- comments
misc.go added +198
@@ -0,0 +1,198 @@
1package devianter
2
3import (
4 "encoding/json"
5 "errors"
6 "fmt"
7 "io"
8 "log"
9 "math"
10 "net/http"
11 "strings"
12)
13
14// функция для высера ошибки в stderr
15func err(txt error) {
16 if txt != nil {
17 println(txt.Error())
18 }
19}
20
21// сокращение для вызова щенка и парсинга жсона
22func ujson(data string, output any) {
23 input, e := puppy(data)
24 err(e)
25
26 eee := json.Unmarshal([]byte(input), output)
27 err(eee)
28}
29
30/* REQUEST SECTION */
31// структура для ответа сервера
32type reqrt struct {
33 Body string
34 Status int
35 Cookies []*http.Cookie
36 Headers http.Header
37}
38
39// функция для совершения запроса
40func request(uri string, other ...string) reqrt {
41 var r reqrt
42
43 // создаём новый запрос
44 cli := &http.Client{}
45 req, e := http.NewRequest("GET", uri, nil)
46 err(e)
47
48 req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0.0")
49
50 // куки и UA-шник
51 if other != nil {
52 for num, rng := range other {
53 switch num {
54 case 1:
55 req.Header.Set("User-Agent", rng)
56 case 0:
57 req.Header.Set("Cookie", rng)
58 }
59 }
60 }
61
62 resp, e := cli.Do(req)
63 err(e)
64 defer resp.Body.Close()
65
66 body, e := io.ReadAll(resp.Body)
67 err(e)
68
69 // заполняем структуру
70 r.Body = string(body)
71 r.Cookies = resp.Cookies()
72 r.Headers = resp.Header
73 r.Status = resp.StatusCode
74
75 return r
76}
77
78/* AVATARS AND EMOJIS */
79func AEmedia(name string, t rune) (string, error) {
80 // список всех возможных расширений
81 var extensions = [3]string{
82 ".jpg",
83 ".png",
84 ".gif",
85 }
86 // надо
87 name = strings.ToLower(name)
88
89 // построение ссылок. билдер потому что он быстрее обычного сложения строк.
90 var b strings.Builder
91 switch t {
92 case 'a':
93 b.WriteString("https://a.deviantart.net/avatars-big/")
94 b.WriteString(name[:1])
95 b.WriteString("/")
96 b.WriteString(name[1:2])
97 b.WriteString("/")
98 case 'e':
99 b.WriteString("https://e.deviantart.net/emoticons/")
100 b.WriteString(name[:1])
101 b.WriteString("/")
102 default:
103 log.Fatalln("Invalid type.\n- 'a' -- avatar;\n- 'e' -- emoji.")
104 }
105 b.WriteString(name)
106
107 // проверка ссылки на доступность
108 for x := 0; x < len(extensions); x++ {
109 req := request(b.String() + extensions[x])
110 if req.Status == 200 {
111 return req.Body, nil
112 }
113 }
114
115 return "", errors.New("User not exists")
116}
117
118/* SEARCH */
119type search struct {
120 Total int `json:"estTotal"`
121 Pages int // only for 'a' scope.
122 Deviations []deviantion
123}
124
125func Search(query, page string, scope rune) (ss search) {
126 var url strings.Builder
127
128 // о5 построение ссылок.
129 switch scope {
130 case 'a':
131 url.WriteString("dabrowse/search/all?q=")
132 case 't':
133 url.WriteString("dabrowse/networkbar/tag/deviations?tag=")
134 default:
135 log.Fatalln("Invalid type.\n- 'a' -- all;\n- 't' -- tag.")
136 }
137
138 url.WriteString(query)
139 url.WriteString("&page=")
140 url.WriteString(page)
141
142 ujson(url.String(), &ss)
143
144 // расчёт, сколько всего страниц по запросу. без токена 417 страниц - максимум
145 for x := 0; x < int(math.Round(float64(ss.Total/25))); x++ {
146 if x <= 417 {
147 ss.Pages = x
148 }
149 }
150
151 return
152}
153
154/* PUPPY aka DeviantArt API */
155func puppy(data string) (string, error) {
156 // получение или обновление токена
157 update := func() (string, string, error) {
158 var cookie string
159 if cookie == "" {
160 req := request("https://www.deviantart.com/_puppy")
161
162 for _, content := range req.Cookies {
163 cookie = content.Raw
164 }
165 }
166
167 req := request("https://www.deviantart.com", cookie)
168 if req.Status != 200 {
169 return "", "", errors.New(req.Body)
170 }
171
172 return cookie, req.Body[strings.Index(req.Body, "window.__CSRF_TOKEN__ = '")+25 : strings.Index(req.Body, "window.__XHR_LOCAL__")-3], nil
173 }
174
175 // использование токена
176 var (
177 cookie, token string
178 )
179 if cookie == "" || token == "" {
180 var e error
181 cookie, token, e = update()
182 if e != nil {
183 return "", e
184 }
185 }
186
187 body := request(
188 fmt.Sprintf("https://www.deviantart.com/_puppy/%s&csrf_token=%s&da_minor_version=20230710", data, token),
189 cookie,
190 )
191
192 // если код ответа не 200, возвращается ошибка
193 if body.Status != 200 {
194 return "", errors.New(body.Body)
195 }
196
197 return body.Body, nil
198}
todo.md added +3
@@ -0,0 +1,3 @@
1- ~~users~~
2- ~~groups~~
3- ~~images in comments~~\ No newline at end of file