123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261 |
- package glance
- import (
- "context"
- "errors"
- "fmt"
- "html"
- "html/template"
- "net/http"
- "net/url"
- "strings"
- "time"
- )
- var (
- redditWidgetHorizontalCardsTemplate = mustParseTemplate("reddit-horizontal-cards.html", "widget-base.html")
- redditWidgetVerticalCardsTemplate = mustParseTemplate("reddit-vertical-cards.html", "widget-base.html")
- )
- type redditWidget struct {
- widgetBase `yaml:",inline"`
- Posts forumPostList `yaml:"-"`
- Subreddit string `yaml:"subreddit"`
- Style string `yaml:"style"`
- ShowThumbnails bool `yaml:"show-thumbnails"`
- ShowFlairs bool `yaml:"show-flairs"`
- SortBy string `yaml:"sort-by"`
- TopPeriod string `yaml:"top-period"`
- Search string `yaml:"search"`
- ExtraSortBy string `yaml:"extra-sort-by"`
- CommentsUrlTemplate string `yaml:"comments-url-template"`
- Limit int `yaml:"limit"`
- CollapseAfter int `yaml:"collapse-after"`
- RequestUrlTemplate string `yaml:"request-url-template"`
- }
- func (widget *redditWidget) initialize() error {
- if widget.Subreddit == "" {
- return errors.New("subreddit is required")
- }
- if widget.Limit <= 0 {
- widget.Limit = 15
- }
- if widget.CollapseAfter == 0 || widget.CollapseAfter < -1 {
- widget.CollapseAfter = 5
- }
- if !isValidRedditSortType(widget.SortBy) {
- widget.SortBy = "hot"
- }
- if !isValidRedditTopPeriod(widget.TopPeriod) {
- widget.TopPeriod = "day"
- }
- if widget.RequestUrlTemplate != "" {
- if !strings.Contains(widget.RequestUrlTemplate, "{REQUEST-URL}") {
- return errors.New("no `{REQUEST-URL}` placeholder specified")
- }
- }
- widget.
- withTitle("/r/" + widget.Subreddit).
- withTitleURL("https://www.reddit.com/r/" + widget.Subreddit + "/").
- withCacheDuration(30 * time.Minute)
- return nil
- }
- func isValidRedditSortType(sortBy string) bool {
- return sortBy == "hot" ||
- sortBy == "new" ||
- sortBy == "top" ||
- sortBy == "rising"
- }
- func isValidRedditTopPeriod(period string) bool {
- return period == "hour" ||
- period == "day" ||
- period == "week" ||
- period == "month" ||
- period == "year" ||
- period == "all"
- }
- func (widget *redditWidget) update(ctx context.Context) {
- // TODO: refactor, use a struct to pass all of these
- posts, err := fetchSubredditPosts(
- widget.Subreddit,
- widget.SortBy,
- widget.TopPeriod,
- widget.Search,
- widget.CommentsUrlTemplate,
- widget.RequestUrlTemplate,
- widget.ShowFlairs,
- )
- if !widget.canContinueUpdateAfterHandlingErr(err) {
- return
- }
- if len(posts) > widget.Limit {
- posts = posts[:widget.Limit]
- }
- if widget.ExtraSortBy == "engagement" {
- posts.calculateEngagement()
- posts.sortByEngagement()
- }
- widget.Posts = posts
- }
- func (widget *redditWidget) Render() template.HTML {
- if widget.Style == "horizontal-cards" {
- return widget.renderTemplate(widget, redditWidgetHorizontalCardsTemplate)
- }
- if widget.Style == "vertical-cards" {
- return widget.renderTemplate(widget, redditWidgetVerticalCardsTemplate)
- }
- return widget.renderTemplate(widget, forumPostsTemplate)
- }
- type subredditResponseJson struct {
- Data struct {
- Children []struct {
- Data struct {
- Id string `json:"id"`
- Title string `json:"title"`
- Upvotes int `json:"ups"`
- Url string `json:"url"`
- Time float64 `json:"created"`
- CommentsCount int `json:"num_comments"`
- Domain string `json:"domain"`
- Permalink string `json:"permalink"`
- Stickied bool `json:"stickied"`
- Pinned bool `json:"pinned"`
- IsSelf bool `json:"is_self"`
- Thumbnail string `json:"thumbnail"`
- Flair string `json:"link_flair_text"`
- ParentList []struct {
- Id string `json:"id"`
- Subreddit string `json:"subreddit"`
- Permalink string `json:"permalink"`
- } `json:"crosspost_parent_list"`
- } `json:"data"`
- } `json:"children"`
- } `json:"data"`
- }
- func templateRedditCommentsURL(template, subreddit, postId, postPath string) string {
- template = strings.ReplaceAll(template, "{SUBREDDIT}", subreddit)
- template = strings.ReplaceAll(template, "{POST-ID}", postId)
- template = strings.ReplaceAll(template, "{POST-PATH}", strings.TrimLeft(postPath, "/"))
- return template
- }
- func fetchSubredditPosts(subreddit, sort, topPeriod, search, commentsUrlTemplate, requestUrlTemplate string, showFlairs bool) (forumPostList, error) {
- query := url.Values{}
- var requestUrl string
- if search != "" {
- query.Set("q", search+" subreddit:"+subreddit)
- query.Set("sort", sort)
- }
- if sort == "top" {
- query.Set("t", topPeriod)
- }
- if search != "" {
- requestUrl = fmt.Sprintf("https://www.reddit.com/search.json?%s", query.Encode())
- } else {
- requestUrl = fmt.Sprintf("https://www.reddit.com/r/%s/%s.json?%s", subreddit, sort, query.Encode())
- }
- if requestUrlTemplate != "" {
- requestUrl = strings.ReplaceAll(requestUrlTemplate, "{REQUEST-URL}", requestUrl)
- }
- request, err := http.NewRequest("GET", requestUrl, nil)
- if err != nil {
- return nil, err
- }
- // Required to increase rate limit, otherwise Reddit randomly returns 429 even after just 2 requests
- setBrowserUserAgentHeader(request)
- responseJson, err := decodeJsonFromRequest[subredditResponseJson](defaultClient, request)
- if err != nil {
- return nil, err
- }
- if len(responseJson.Data.Children) == 0 {
- return nil, fmt.Errorf("no posts found")
- }
- posts := make(forumPostList, 0, len(responseJson.Data.Children))
- for i := range responseJson.Data.Children {
- post := &responseJson.Data.Children[i].Data
- if post.Stickied || post.Pinned {
- continue
- }
- var commentsUrl string
- if commentsUrlTemplate == "" {
- commentsUrl = "https://www.reddit.com" + post.Permalink
- } else {
- commentsUrl = templateRedditCommentsURL(commentsUrlTemplate, subreddit, post.Id, post.Permalink)
- }
- forumPost := forumPost{
- Title: html.UnescapeString(post.Title),
- DiscussionUrl: commentsUrl,
- TargetUrlDomain: post.Domain,
- CommentCount: post.CommentsCount,
- Score: post.Upvotes,
- TimePosted: time.Unix(int64(post.Time), 0),
- }
- if post.Thumbnail != "" && post.Thumbnail != "self" && post.Thumbnail != "default" {
- forumPost.ThumbnailUrl = post.Thumbnail
- }
- if !post.IsSelf {
- forumPost.TargetUrl = post.Url
- }
- if showFlairs && post.Flair != "" {
- forumPost.Tags = append(forumPost.Tags, post.Flair)
- }
- if len(post.ParentList) > 0 {
- forumPost.IsCrosspost = true
- forumPost.TargetUrlDomain = "r/" + post.ParentList[0].Subreddit
- if commentsUrlTemplate == "" {
- forumPost.TargetUrl = "https://www.reddit.com" + post.ParentList[0].Permalink
- } else {
- forumPost.TargetUrl = templateRedditCommentsURL(
- commentsUrlTemplate,
- post.ParentList[0].Subreddit,
- post.ParentList[0].Id,
- post.ParentList[0].Permalink,
- )
- }
- }
- posts = append(posts, forumPost)
- }
- return posts, nil
- }
|