krz/devianter

A DeviantArt guest API library for Go.

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

v0.1.0: misc.go · raw

  1package devianter
  2
  3import (
  4	"encoding/json"
  5	"errors"
  6	"io"
  7	"log"
  8	"math"
  9	"net/http"
 10	"strconv"
 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' and 'g' scope.
122	Results []deviantion `json:"deviations,results"`
123}
124
125func Search(query string, page int, scope rune, user ...string) (ss search, e error) {
126	var url strings.Builder
127	e = nil
128
129	// о5 построение ссылок.
130	switch scope {
131	case 'a': // поиск артов по названию
132		url.WriteString("dabrowse/search/all?q=")
133	case 't': // поиск артов по тегам
134		url.WriteString("dabrowse/networkbar/tag/deviations?tag=")
135	case 'g': // поиск артов пользователя или группы
136		if user != nil {
137			url.WriteString("dashared/gallection/search?username=")
138			for _, a := range user {
139				url.WriteString(a)
140			}
141			url.WriteString("&type=gallery&order=most-recent&init=true&limit=50&q=")
142		} else {
143			e = errors.New("Missing username (last argument)")
144			return
145		}
146	default:
147		log.Fatalln("Invalid type.\n- 'a' -- all;\n- 't' -- tag;\n- 'g' - gallery.")
148	}
149
150	url.WriteString(query)
151	if scope != 'g' { // если область поиска не равна поиску по группам, то активируется этот код
152		url.WriteString("&page=")
153	} else { // иначе вместо страницы будет оффсет и страница умножится на 50
154		url.WriteString("&offset=")
155		page = 50 * page
156	}
157	url.WriteString(strconv.Itoa(page))
158
159	ujson(url.String(), &ss)
160
161	// расчёт, сколько всего страниц по запросу. без токена 417 страниц - максимум
162	for x := 0; x < int(math.Round(float64(ss.Total/25))); x++ {
163		if x <= 417 {
164			ss.Pages = x
165		}
166	}
167
168	return
169}
170
171/* PUPPY aka DeviantArt API */
172func puppy(data string) (string, error) {
173	// получение или обновление токена
174	update := func() (string, string, error) {
175		var cookie string
176		if cookie == "" {
177			req := request("https://www.deviantart.com/_puppy")
178
179			for _, content := range req.Cookies {
180				cookie = content.Raw
181			}
182		}
183
184		req := request("https://www.deviantart.com", cookie)
185		if req.Status != 200 {
186			return "", "", errors.New(req.Body)
187		}
188
189		return cookie, req.Body[strings.Index(req.Body, "window.__CSRF_TOKEN__ = '")+25 : strings.Index(req.Body, "window.__XHR_LOCAL__")-3], nil
190	}
191
192	// использование токена
193	var (
194		cookie, token string
195	)
196	if cookie == "" || token == "" {
197		var e error
198		cookie, token, e = update()
199		if e != nil {
200			return "", e
201		}
202	}
203
204	var url strings.Builder
205	url.WriteString("https://www.deviantart.com/_puppy/")
206	url.WriteString(data)
207	url.WriteString("&csrf_token=")
208	url.WriteString(token)
209	url.WriteString("&da_minor_version=20230710")
210
211	body := request(url.String(), cookie)
212
213	// если код ответа не 200, возвращается ошибка
214	if body.Status != 200 {
215		return "", errors.New(body.Body)
216	}
217
218	return body.Body, nil
219}