package formatter import ( "bytes" "encoding/json" "fmt" "html" "strings" "text/tabwriter" "time" "git.db.org.ai/dborg/internal/models" ) func FormatRedditResults(resp interface{}, asJSON bool) (string, error) { if asJSON { data, err := json.MarshalIndent(resp, "", " ") if err != nil { return "", fmt.Errorf("failed to marshal JSON: %w", err) } return string(data), nil } var credits models.RedditCredits var resultsData interface{} var dataType string var identifier string switch r := resp.(type) { case *models.SubredditResponse: credits = r.Credits resultsData = r.Results dataType = r.Type identifier = r.Subreddit case *models.UserResponse: credits = r.Credits resultsData = r.Results dataType = r.Type identifier = r.Username default: return "", fmt.Errorf("unsupported response type") } var buf bytes.Buffer switch dataType { case "comments": formatRedditComments(&buf, resultsData, dataType, identifier) case "posts": formatRedditPosts(&buf, resultsData, dataType, identifier) case "about": formatRedditUserAbout(&buf, resultsData, dataType, identifier) default: buf.WriteString(fmt.Sprintf("\n%s\n", Bold(Cyan(fmt.Sprintf("Reddit %s - %s", dataType, identifier))))) buf.WriteString(fmt.Sprintf("%s\n\n", Gray(strings.Repeat("─", 80)))) if resultsData != nil { resultsJSON, err := json.MarshalIndent(resultsData, "", " ") if err != nil { return "", fmt.Errorf("failed to marshal results: %w", err) } buf.WriteString(string(resultsJSON)) buf.WriteString("\n\n") } else { buf.WriteString(fmt.Sprintf("%s\n\n", Gray("No results found"))) } } if credits.Unlimited { buf.WriteString(fmt.Sprintf("%s: %s\n", Dim("Credits"), Green("Unlimited"))) } else { buf.WriteString(fmt.Sprintf("%s: %s\n", Dim("Credits Remaining"), FormatCredits(int64(credits.Remaining)))) } return buf.String(), nil } func formatRedditUserAbout(buf *bytes.Buffer, resultsData interface{}, dataType, identifier string) { fmt.Fprintf(buf, "\n%s\n", Bold(Cyan(fmt.Sprintf("Reddit User - u/%s", identifier)))) fmt.Fprintf(buf, "%s\n\n", Gray(strings.Repeat("─", 80))) userData, ok := resultsData.(map[string]interface{}) if !ok { fmt.Fprintf(buf, "%s\n", Gray("Invalid data structure")) return } data, ok := userData["data"].(map[string]interface{}) if !ok { fmt.Fprintf(buf, "%s\n", Gray("No user data found")) return } w := tabwriter.NewWriter(buf, 0, 0, 2, ' ', 0) if name := getStringValue(data, "name"); name != "" { fmt.Fprintf(w, "%s\t%s\n", Cyan("Username:"), Bold(fmt.Sprintf("u/%s", name))) } if id := getStringValue(data, "id"); id != "" { fmt.Fprintf(w, "%s\t%s\n", Cyan("User ID:"), Dim(id)) } if created, ok := data["created_utc"].(float64); ok { timestamp := time.Unix(int64(created), 0) age := time.Since(timestamp) years := int(age.Hours() / 24 / 365) fmt.Fprintf(w, "%s\t%s (%s)\n", Cyan("Account Age:"), Gray(timestamp.Format("2006-01-02")), Yellow(fmt.Sprintf("%d years", years))) } totalKarma := getIntValue(data, "total_karma") linkKarma := getIntValue(data, "link_karma") commentKarma := getIntValue(data, "comment_karma") awardeeKarma := getIntValue(data, "awardee_karma") awarderKarma := getIntValue(data, "awarder_karma") fmt.Fprintf(w, "%s\t%s\n", Cyan("Total Karma:"), Bold(Green(fmt.Sprintf("%d", totalKarma)))) fmt.Fprintf(w, "%s\t%s\n", Cyan(" Post Karma:"), Green(fmt.Sprintf("%d", linkKarma))) fmt.Fprintf(w, "%s\t%s\n", Cyan(" Comment Karma:"), Green(fmt.Sprintf("%d", commentKarma))) if awardeeKarma > 0 { fmt.Fprintf(w, "%s\t%s\n", Cyan(" Awardee Karma:"), Green(fmt.Sprintf("%d", awardeeKarma))) } if awarderKarma > 0 { fmt.Fprintf(w, "%s\t%s\n", Cyan(" Awarder Karma:"), Green(fmt.Sprintf("%d", awarderKarma))) } badges := []string{} if isMod, ok := data["is_mod"].(bool); ok && isMod { badges = append(badges, Green("Moderator")) } if isEmployee, ok := data["is_employee"].(bool); ok && isEmployee { badges = append(badges, Red("Reddit Employee")) } if isGold, ok := data["is_gold"].(bool); ok && isGold { badges = append(badges, Yellow("Premium")) } if verified, ok := data["verified"].(bool); ok && verified { badges = append(badges, Blue("Verified Email")) } if len(badges) > 0 { fmt.Fprintf(w, "%s\t%s\n", Cyan("Badges:"), strings.Join(badges, " • ")) } if acceptFollowers, ok := data["accept_followers"].(bool); ok { if acceptFollowers { fmt.Fprintf(w, "%s\t%s\n", Cyan("Followers:"), Green("Enabled")) } else { fmt.Fprintf(w, "%s\t%s\n", Cyan("Followers:"), Gray("Disabled")) } } w.Flush() if subreddit, ok := data["subreddit"].(map[string]interface{}); ok { if publicDesc := getStringValue(subreddit, "public_description"); publicDesc != "" { fmt.Fprintf(buf, "\n%s\n", Cyan("Bio:")) publicDesc = html.UnescapeString(publicDesc) lines := strings.Split(publicDesc, "\n") for _, line := range lines { if len(line) > 78 { wrapped := wrapText(line, 76) for _, wLine := range wrapped { fmt.Fprintf(buf, " %s\n", wLine) } } else { fmt.Fprintf(buf, " %s\n", line) } } } } if iconImg := getStringValue(data, "icon_img"); iconImg != "" { fmt.Fprintf(buf, "\n%s %s\n", Cyan("Avatar:"), Blue(iconImg)) } fmt.Fprintf(buf, "\n") } func formatRedditPosts(buf *bytes.Buffer, resultsData interface{}, dataType, identifier string) { fmt.Fprintf(buf, "\n%s\n", Bold(Cyan(fmt.Sprintf("Reddit %s - %s", dataType, identifier)))) fmt.Fprintf(buf, "%s\n\n", Gray(strings.Repeat("─", 80))) listing, ok := resultsData.(map[string]interface{}) if !ok { fmt.Fprintf(buf, "%s\n", Gray("Invalid data structure")) return } data, ok := listing["data"].(map[string]interface{}) if !ok { fmt.Fprintf(buf, "%s\n", Gray("No data found")) return } children, ok := data["children"].([]interface{}) if !ok || len(children) == 0 { fmt.Fprintf(buf, "%s\n", Gray("No posts found")) return } for i, child := range children { childMap, ok := child.(map[string]interface{}) if !ok { continue } postData, ok := childMap["data"].(map[string]interface{}) if !ok { continue } formatSinglePost(buf, postData, i+1) } } func formatRedditComments(buf *bytes.Buffer, resultsData interface{}, dataType, identifier string) { fmt.Fprintf(buf, "\n%s\n", Bold(Cyan(fmt.Sprintf("Reddit %s - %s", dataType, identifier)))) fmt.Fprintf(buf, "%s\n\n", Gray(strings.Repeat("─", 80))) listing, ok := resultsData.(map[string]interface{}) if !ok { fmt.Fprintf(buf, "%s\n", Gray("Invalid data structure")) return } data, ok := listing["data"].(map[string]interface{}) if !ok { fmt.Fprintf(buf, "%s\n", Gray("No data found")) return } children, ok := data["children"].([]interface{}) if !ok || len(children) == 0 { fmt.Fprintf(buf, "%s\n", Gray("No comments found")) return } for i, child := range children { childMap, ok := child.(map[string]interface{}) if !ok { continue } commentData, ok := childMap["data"].(map[string]interface{}) if !ok { continue } formatSingleComment(buf, commentData, i+1) } } func formatSinglePost(buf *bytes.Buffer, post map[string]interface{}, num int) { fmt.Fprintf(buf, "%s\n", Bold(Yellow(fmt.Sprintf("Post #%d", num)))) title := getStringValue(post, "title") if title != "" { title = html.UnescapeString(title) fmt.Fprintf(buf, "%s\n", Bold(Green(title))) } w := tabwriter.NewWriter(buf, 0, 0, 2, ' ', 0) author := getStringValue(post, "author") subreddit := getStringValue(post, "subreddit") if author != "" { fmt.Fprintf(w, "%s\t%s", Cyan("Author:"), Bold(fmt.Sprintf("u/%s", author))) if subreddit != "" { fmt.Fprintf(w, " in %s", Bold(fmt.Sprintf("r/%s", subreddit))) } fmt.Fprintf(w, "\n") } if created, ok := post["created_utc"].(float64); ok { timestamp := time.Unix(int64(created), 0) fmt.Fprintf(w, "%s\t%s\n", Cyan("Posted:"), Gray(timestamp.Format("2006-01-02 15:04:05 MST"))) } score := getIntValue(post, "score") upvoteRatio := 0.0 if ratio, ok := post["upvote_ratio"].(float64); ok { upvoteRatio = ratio } fmt.Fprintf(w, "%s\t%s", Cyan("Score:"), formatScore(score)) if upvoteRatio > 0 { fmt.Fprintf(w, " (%s upvoted)", Dim(fmt.Sprintf("%.0f%%", upvoteRatio*100))) } fmt.Fprintf(w, "\n") numComments := getIntValue(post, "num_comments") fmt.Fprintf(w, "%s\t%s\n", Cyan("Comments:"), Yellow(fmt.Sprintf("%d", numComments))) if stickied, ok := post["stickied"].(bool); ok && stickied { fmt.Fprintf(w, "%s\t%s\n", Cyan("Status:"), Green("Stickied")) } if linkFlair := getStringValue(post, "link_flair_text"); linkFlair != "" { fmt.Fprintf(w, "%s\t%s\n", Cyan("Flair:"), Magenta(linkFlair)) } domain := getStringValue(post, "domain") url := getStringValue(post, "url") isSelf, _ := post["is_self"].(bool) if !isSelf && url != "" { fmt.Fprintf(w, "%s\t%s\n", Cyan("Link:"), Blue(url)) if domain != "" && !strings.HasPrefix(domain, "self.") { fmt.Fprintf(w, "%s\t%s\n", Cyan("Domain:"), Dim(domain)) } } if permalink := getStringValue(post, "permalink"); permalink != "" { fmt.Fprintf(w, "%s\t%s\n", Cyan("Reddit:"), Blue(fmt.Sprintf("https://reddit.com%s", permalink))) } w.Flush() selftext := getStringValue(post, "selftext") if selftext != "" && selftext != "[removed]" && selftext != "[deleted]" { fmt.Fprintf(buf, "\n%s\n", Cyan("Content:")) selftext = html.UnescapeString(selftext) lines := strings.Split(selftext, "\n") displayedLines := 0 maxLines := 15 for _, line := range lines { if displayedLines >= maxLines { remaining := len(lines) - displayedLines fmt.Fprintf(buf, " %s\n", Dim(fmt.Sprintf("... (%d more lines)", remaining))) break } if len(line) > 80 { wrapped := wrapText(line, 78) for _, wLine := range wrapped { if displayedLines >= maxLines { break } fmt.Fprintf(buf, " %s\n", wLine) displayedLines++ } } else { fmt.Fprintf(buf, " %s\n", line) displayedLines++ } } } if edited := post["edited"]; edited != nil && edited != false { fmt.Fprintf(buf, "\n%s\n", Dim("(edited)")) } fmt.Fprintf(buf, "\n%s\n\n", Gray(strings.Repeat("─", 80))) } func formatSingleComment(buf *bytes.Buffer, comment map[string]interface{}, num int) { w := tabwriter.NewWriter(buf, 0, 0, 1, ' ', 0) fmt.Fprintf(buf, "%s\n", Bold(Yellow(fmt.Sprintf("Comment #%d", num)))) author := getStringValue(comment, "author") if author != "" { fmt.Fprintf(w, "%s\t%s\n", Cyan("Author:"), Bold(author)) } subreddit := getStringValue(comment, "subreddit") if subreddit != "" { fmt.Fprintf(w, "%s\tr/%s\n", Cyan("Subreddit:"), subreddit) } if created, ok := comment["created_utc"].(float64); ok { timestamp := time.Unix(int64(created), 0) fmt.Fprintf(w, "%s\t%s\n", Cyan("Posted:"), Gray(timestamp.Format("2006-01-02 15:04:05 MST"))) } score := getIntValue(comment, "score") fmt.Fprintf(w, "%s\t%s\n", Cyan("Score:"), formatScore(score)) if controversiality, ok := comment["controversiality"].(float64); ok && controversiality > 0 { fmt.Fprintf(w, "%s\t%s\n", Cyan("Controversial:"), Yellow("Yes")) } w.Flush() if linkTitle := getStringValue(comment, "link_title"); linkTitle != "" { fmt.Fprintf(buf, "%s %s\n", Cyan("Post:"), Bold(linkTitle)) } if permalink := getStringValue(comment, "permalink"); permalink != "" { fmt.Fprintf(buf, "%s %s\n", Cyan("Link:"), Blue(fmt.Sprintf("https://reddit.com%s", permalink))) } body := getStringValue(comment, "body") if body != "" { fmt.Fprintf(buf, "\n%s\n", Cyan("Comment:")) body = html.UnescapeString(body) lines := strings.Split(body, "\n") for _, line := range lines { if len(line) > 80 { wrapped := wrapText(line, 78) for _, wLine := range wrapped { fmt.Fprintf(buf, " %s\n", Green(wLine)) } } else { fmt.Fprintf(buf, " %s\n", Green(line)) } } } if edited := comment["edited"]; edited != nil && edited != false { fmt.Fprintf(buf, "\n%s\n", Dim("(edited)")) } fmt.Fprintf(buf, "\n%s\n\n", Gray(strings.Repeat("─", 80))) } func getStringValue(data map[string]interface{}, key string) string { if val, ok := data[key].(string); ok { return val } return "" } func getIntValue(data map[string]interface{}, key string) int { if val, ok := data[key].(float64); ok { return int(val) } return 0 } func formatScore(score int) string { if score > 0 { return Green(fmt.Sprintf("+%d", score)) } else if score < 0 { return Red(fmt.Sprintf("%d", score)) } return Dim("0") }