krz/devianter
A DeviantArt guest API library for Go.
clone: git clone https://gitbay.org/krz/devianter.git
v0.3.2: comments.go · raw
1package devianter
2
3import (
4 "encoding/json"
5 "net/url"
6 "strconv"
7 "strings"
8)
9
10// Thread is a single comment, despite the name. Replies are not nested inside
11// it: a thread arrives flattened into [Comments].Thread, and the shape is
12// recovered through Parent, which holds the ID of the comment being replied to
13// and is 0 for a top-level comment.
14type Thread struct {
15 Replies, Likes int
16 ID int `json:"commentId"`
17 Parent int `json:"parentId"`
18
19 Posted timeStamp
20 // Author reports whether the commenter is the author of the deviation being
21 // commented on.
22 Author bool `json:"isAuthorHighlited"`
23
24 Desctiption string
25
26 // Comment is the comment's plain text, which [GetComments] extracts from
27 // TextContent. Prefer it; TextContent is the unprocessed original.
28 Comment string
29
30 TextContent Text
31
32 User struct {
33 Username string
34 Banned bool `json:"isBanned"`
35 }
36}
37
38// Comments is one page of comments. Thread holds the comments themselves,
39// flattened rather than nested; Total counts every comment on the item, not just
40// this page. Cursor resumes from the end of this page and HasMore reports
41// whether anything remains.
42type Comments struct {
43 Cursor string
44 PrevOffset int
45 HasMore, HasLess bool
46
47 Total int
48 Thread []Thread
49}
50
51// GetComments retrieves comments on an item, 50 per page, with each comment's
52// plain text extracted into [Thread].Comment.
53//
54// typ selects what postid refers to: 1 for comments on a deviation, 4 for those
55// on a user's or group's profile wall. cursor resumes from a previous call's
56// [Comments].Cursor; pass an empty string to start from the newest comment.
57//
58// page is an offset from cursor rather than an absolute page number, and it is
59// walked one request at a time: page 5 costs six round-trips and returns only
60// the sixth page. Paginating by feeding each result's Cursor back in with page 0
61// costs one request per page, and is the cheaper way to walk a long thread.
62func GetComments(postid string, cursor string, page int, typ int) (cmmts Comments, err Error) {
63 for x := 0; x <= page; x++ {
64 err = ujson(
65 "dashared/comments/thread?typeid="+strconv.Itoa(typ)+
66 "&itemid="+postid+"&maxdepth=1000&order=newest"+
67 "&limit=50&cursor="+url.QueryEscape(cursor),
68 &cmmts,
69 )
70
71 cursor = cmmts.Cursor
72
73 for i := 0; i < len(cmmts.Thread); i++ {
74 cmmts.Thread[i].Comment = flattenComment(cmmts.Thread[i].TextContent.Html.Markup)
75 }
76 }
77
78 return
79}
80
81// flattenComment renders a body of user-written markup as plain text, be it a
82// comment or a deviation's description. Bodies are JSON inside JSON: newer ones
83// are a Draft.js document encoded into the markup string, older ones are plain
84// HTML, which passes through unchanged. Markup that does not parse, and empty
85// markup, also pass through.
86//
87// A Draft.js document is a list of blocks, which are block-level elements
88// (paragraphs, list items); they are joined with newlines, one block per line.
89func flattenComment(m string) string {
90 l := len(m)
91 if l == 0 || m[0] != '{' || m[l-1] != '}' {
92 return m
93 }
94
95 var content struct {
96 Blocks []struct {
97 Text string
98 }
99 }
100
101 e := json.Unmarshal([]byte(m), &content)
102 try(e)
103
104 if len(content.Blocks) == 0 {
105 return m
106 }
107
108 var b strings.Builder
109 for i, a := range content.Blocks {
110 if i > 0 {
111 b.WriteString("\n")
112 }
113 b.WriteString(a.Text)
114 }
115 return b.String()
116}