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