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}