Commit 66ab740d7f
Unsigned
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 @@ | ||
| 1 | package devianter | |
| 2 | ||
| 3 | import ( | |
| 4 | "encoding/json" | |
| 5 | "strconv" | |
| 6 | "strings" | |
| 7 | ) | |
| 8 | ||
| 9 | type 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 | // функция для обработки комментариев поста, пользователя, группы и многого другого | |
| 39 | func 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 @@ | ||
| 1 | package devianter | |
| 2 | ||
| 3 | import ( | |
| 4 | "encoding/json" | |
| 5 | "strconv" | |
| 6 | timelib "time" | |
| 7 | ) | |
| 8 | ||
| 9 | // хрень для парсинга времени публикации | |
| 10 | type time struct { | |
| 11 | timelib.Time | |
| 12 | } | |
| 13 | ||
| 14 | func (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 | // самая главная структура для поста | |
| 23 | type 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 | // её выпердыши | |
| 47 | type media struct { | |
| 48 | BaseUri string | |
| 49 | Token []string | |
| 50 | Types []struct { | |
| 51 | T string | |
| 52 | H, W int | |
| 53 | } | |
| 54 | } | |
| 55 | ||
| 56 | type text struct { | |
| 57 | Html struct { | |
| 58 | Markup, Type string | |
| 59 | } | |
| 60 | } | |
| 61 | ||
| 62 | type 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 поста и имя пользователя. | |
| 80 | func 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 @@ | ||
| 1 | package main | |
| 2 | ||
| 3 | import ( | |
| 4 | "git.macaw.me/skunky/devianter" | |
| 5 | ) | |
| 6 | ||
| 7 | func 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 @@ | ||
| 1 | module git.macaw.me/skunky/devianter | |
| 2 | ||
| 3 | go 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 @@ | ||
| 1 | package devianter | |
| 2 | ||
| 3 | import ( | |
| 4 | "encoding/json" | |
| 5 | "errors" | |
| 6 | "fmt" | |
| 7 | "io" | |
| 8 | "log" | |
| 9 | "math" | |
| 10 | "net/http" | |
| 11 | "strings" | |
| 12 | ) | |
| 13 | ||
| 14 | // функция для высера ошибки в stderr | |
| 15 | func err(txt error) { | |
| 16 | if txt != nil { | |
| 17 | println(txt.Error()) | |
| 18 | } | |
| 19 | } | |
| 20 | ||
| 21 | // сокращение для вызова щенка и парсинга жсона | |
| 22 | func 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 | // структура для ответа сервера | |
| 32 | type reqrt struct { | |
| 33 | Body string | |
| 34 | Status int | |
| 35 | Cookies []*http.Cookie | |
| 36 | Headers http.Header | |
| 37 | } | |
| 38 | ||
| 39 | // функция для совершения запроса | |
| 40 | func 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 */ | |
| 79 | func 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 */ | |
| 119 | type search struct { | |
| 120 | Total int `json:"estTotal"` | |
| 121 | Pages int // only for 'a' scope. | |
| 122 | Deviations []deviantion | |
| 123 | } | |
| 124 | ||
| 125 | func 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 */ | |
| 155 | func 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 | ||