This repository has been archived on 2022-03-09. You can view files and clone it, but cannot push or open issues or pull requests.
snoo2nebby/vendor/github.com/turnage/graw/reddit/parse.go

329 lines
8.8 KiB
Go

package reddit
import (
"encoding/json"
"fmt"
"github.com/mitchellh/mapstructure"
)
const (
listingKind = "Listing"
postKind = "t3"
commentKind = "t1"
messageKind = "t4"
moreKind = "more"
)
// author fields and body fields are set to the deletedKey if the user deletes
// their post.
const deletedKey = "[deleted]"
// thing is a Reddit type that holds all of their subtypes.
type thing struct {
Kind string `json:"kind"`
Data map[string]interface{} `json:"data"`
}
type listing struct {
Children []thing `json:"children,omitempty"`
}
type more struct {
Errors []interface{} `json:"errors,omitempty"`
Data []thing `json:"data"`
}
// comment wraps the user facing Comment type with a Replies field for
// intermediate parsing.
type comment struct {
Comment `mapstructure:",squash"`
Replies thing `mapstructure:"replies"`
}
// parser parses Reddit responses..
type parser interface {
// parse parses any Reddit response and provides the elements in it.
parse(blob json.RawMessage) ([]*Comment, []*Post, []*Message, []*More, error)
parse_submitted(blob json.RawMessage) (Submission, error)
}
type parserImpl struct{}
func newParser() parser {
return &parserImpl{}
}
// parse parses any Reddit response and provides the elements in it.
func (p *parserImpl) parse(
blob json.RawMessage,
) ([]*Comment, []*Post, []*Message, []*More, error) {
comments, posts, msgs, mores, listingErr := parseRawListing(blob)
if listingErr == nil {
return comments, posts, msgs, mores, nil
}
post, threadErr := parseThread(blob)
if threadErr == nil {
return nil, []*Post{post}, nil, nil, nil
}
comments, mores, moreErr := parseMoreChildren(blob)
if moreErr == nil {
return comments, nil, nil, mores, nil
}
return nil, nil, nil, nil, fmt.Errorf(
"failed to parse as listing [%v], thread [%v], or more [%v]",
listingErr, threadErr, moreErr,
)
}
// parse_submitted parses a response from reddit describing
// the status of some resource that was submitted
func (p *parserImpl) parse_submitted(blob json.RawMessage) (Submission, error) {
var wrapped map[string]interface{}
err := json.Unmarshal(blob, &wrapped)
if err != nil {
return Submission{}, err
}
wrapped = wrapped["json"].(map[string]interface{})
if len(wrapped["errors"].([]interface{})) != 0 {
return Submission{}, fmt.Errorf("API errors were returned: %v", wrapped["errors"])
}
data := wrapped["data"].(map[string]interface{})
// Comment submissions are further wrapped in a things block
// because of ... something? There only appears to be a single thing
// This transformes var data to be the data of the single thing
// This also mirrors https://reddit.com/ + permalink -> url
things, has_things := data["things"].([]interface{})
if has_things && len(things) == 1 {
data = things[0].(map[string]interface{})["data"].(map[string]interface{})
data["url"] = fmt.Sprintf("https://reddit.com%s", data["permalink"])
}
var submission Submission
err = mapstructure.Decode(data, &submission)
return submission, err
}
// parseRawListing parses a listing json blob and returns the elements in it.
func parseRawListing(
blob json.RawMessage,
) ([]*Comment, []*Post, []*Message, []*More, error) {
var activityListing thing
if err := json.Unmarshal(blob, &activityListing); err != nil {
return nil, nil, nil, nil, err
}
return parseListing(&activityListing)
}
// parseMoreChildren parses the json blob from /api/morechildren calls and returns the elements in it.
func parseMoreChildren(
blob json.RawMessage,
) ([]*Comment, []*More, error) {
var wrapped map[string]interface{}
err := json.Unmarshal(blob, &wrapped)
if err != nil {
return nil, nil, err
}
wrapped = wrapped["json"].(map[string]interface{})
if len(wrapped["errors"].([]interface{})) != 0 {
return nil, nil, fmt.Errorf("API errors were returned: %v", wrapped["errors"])
}
data := wrapped["data"].(map[string]interface{})
// More submissions are further wrapped in a things block,
// so reorganize data so that it makes sense
things, hasThings := data["things"].([]interface{})
if hasThings {
data["data"] = things
delete(data, "things")
} else {
return nil, nil, fmt.Errorf("No thing types returned")
}
var m more
err = mapstructure.Decode(data, &m)
if err != nil {
return nil, nil, err
} else if m.Errors != nil {
return nil, nil, fmt.Errorf("%v", m.Errors)
}
comments, _, _, mores, err := parseChildren(m.Data)
return comments, mores, err
}
// parseThread parses a post from a thread json blob returned by Reddit.
//
// Reddit structures this as two things in an array, the first thing being a
// listing with only the post and the second thing being a listing of comments.
func parseThread(blob json.RawMessage) (*Post, error) {
var listings [2]thing
if err := json.Unmarshal(blob, &listings); err != nil {
return nil, err
}
_, posts, _, _, err := parseListing(&listings[0])
if err != nil {
return nil, err
}
if len(posts) != 1 {
return nil, fmt.Errorf("expected 1 post; found %d", len(posts))
}
comments, _, _, mores, err := parseListing(&listings[1])
if err != nil {
return nil, err
}
// a submission should only have one more object
if len(mores) == 1 {
posts[0].More = mores[0]
} else if len(mores) > 1 {
return nil, fmt.Errorf("expected 1 more; found %d", len(mores))
}
posts[0].Replies = comments
return posts[0], nil
}
// parseListing parses a Reddit listing type and returns the elements inside it.
func parseListing(t *thing) ([]*Comment, []*Post, []*Message, []*More, error) {
if t.Kind != listingKind {
return nil, nil, nil, nil, fmt.Errorf("thing is not listing")
}
l := &listing{}
if err := mapstructure.Decode(t.Data, l); err != nil {
return nil, nil, nil, nil, mapDecodeError(err, t.Data)
}
return parseChildren(l.Children)
}
// parseChildren returns a list of parsed objects from the given list of things
func parseChildren(children []thing) ([]*Comment, []*Post, []*Message, []*More, error) {
comments := []*Comment{}
posts := []*Post{}
msgs := []*Message{}
mores := []*More{}
err := error(nil)
for _, c := range children {
if err != nil {
break
}
var comment *Comment
var post *Post
var msg *Message
var more *More
// Reddit sets the "Kind" field of comments in the inbox, which
// have only Message and not Comment fields, to commentKind. The
// give away in this case is that comments in message form have
// a field called "was_comment". Reddit does this because they
// hate programmers.
if c.Kind == messageKind || c.Data["was_comment"] != nil {
msg, err = parseMessage(&c)
msgs = append(msgs, msg)
} else if c.Kind == commentKind {
comment, err = parseComment(&c)
comments = append(comments, comment)
} else if c.Kind == postKind {
post, err = parsePost(&c)
posts = append(posts, post)
} else if c.Kind == moreKind {
more, err = parseMore(&c)
mores = append(mores, more)
}
}
return comments, posts, msgs, mores, err
}
// parseComment parses a comment into the user facing Comment struct.
func parseComment(t *thing) (*Comment, error) {
// Reddit makes the replies field a string if it is empty, just to make
// it harder for programmers who like static type systems.
value, present := t.Data["replies"]
if present {
if str, ok := value.(string); ok && str == "" {
delete(t.Data, "replies")
}
}
// Similarly, edited is false if a post hasn't been edited and a timestamp
// otherwise.
value, present = t.Data["edited"]
if present {
if _, ok := value.(bool); ok {
delete(t.Data, "edited")
}
}
c := &comment{}
if err := mapstructure.Decode(t.Data, c); err != nil {
return nil, mapDecodeError(err, t.Data)
}
var err error
var mores []*More
if c.Replies.Kind == listingKind {
c.Comment.Replies, _, _, mores, err = parseListing(&c.Replies)
// a commment branch should only have one more object
if len(mores) == 1 {
c.Comment.More = mores[0]
} else if len(mores) > 1 {
return nil, fmt.Errorf("expected 1 more; found %d", len(mores))
}
}
c.Comment.Deleted = c.Comment.Body == deletedKey
return &c.Comment, err
}
// parsePost parses a post into the user facing Post struct.
func parsePost(t *thing) (*Post, error) {
p := &Post{}
if err := mapstructure.Decode(t.Data, p); err != nil {
return nil, mapDecodeError(err, t.Data)
}
p.Deleted = p.SelfText == deletedKey
return p, nil
}
// parseMessage parses a message into the user facing Message struct.
func parseMessage(t *thing) (*Message, error) {
m := &Message{}
return m, mapstructure.Decode(t.Data, m)
}
// parseMore parses a more comment list into the user facing More struct.
func parseMore(t *thing) (*More, error) {
m := &More{}
if err := mapstructure.Decode(t.Data, m); err != nil {
return nil, mapDecodeError(err, t.Data)
}
return m, nil
}
func mapDecodeError(err error, val interface{}) error {
return fmt.Errorf(
"failed to decode json map into struct: %v; value: %v",
err, val,
)
}