From 8ec1175fe4d7ebe56a78819ebfaea280bf742cfe Mon Sep 17 00:00:00 2001 From: Tigor Hutasuhut Date: Wed, 10 Apr 2024 17:13:07 +0700 Subject: [PATCH] reddit: added repository --- api/download_subreddit_images.go | 16 ++- api/download_subreddit_posts.go | 16 +++ api/reddit/client.go | 9 ++ api/reddit/fancy_reader.go | 1 + api/reddit/get_posts.go | 64 ++++++++++ api/reddit/post.go | 191 +++++++++++++++++++++++++++++ api/reddit/reddit.go | 8 ++ api/subredditpost/subredditpost.go | 191 +++++++++++++++++++++++++++++ pkg/errs/errs.go | 4 + 9 files changed, 496 insertions(+), 4 deletions(-) create mode 100644 api/download_subreddit_posts.go create mode 100644 api/reddit/client.go create mode 100644 api/reddit/fancy_reader.go create mode 100644 api/reddit/get_posts.go create mode 100644 api/reddit/post.go create mode 100644 api/reddit/reddit.go create mode 100644 api/subredditpost/subredditpost.go diff --git a/api/download_subreddit_images.go b/api/download_subreddit_images.go index c8178c1..dc60365 100644 --- a/api/download_subreddit_images.go +++ b/api/download_subreddit_images.go @@ -3,6 +3,7 @@ package api import ( "context" "errors" + "net/http" "github.com/tigorlazuardi/redmage/db/queries" "github.com/tigorlazuardi/redmage/pkg/errs" @@ -12,16 +13,23 @@ type DownloadSubredditParams struct { Countback int NSFW bool Devices []queries.Device + Type int } var ( - ErrNoDevices = errors.New("api: downloading subreddit images requires at least one device") - ErrDownloadDirNotSet = errors.New("api: downloading subreddit images require download directory to be set") + ErrNoDevices = errors.New("api: no devices set") + ErrDownloadDirNotSet = errors.New("api: download directory not set") ) func (api *API) DownloadSubredditImages(ctx context.Context, subredditName string, params DownloadSubredditParams) error { - if len(params.Devices) == 0 { - return errs.Wrap(ErrNoDevices) + downloadDir := api.config.String("download.directory") + if downloadDir == "" { + return errs.Wrapw(ErrDownloadDirNotSet, "download directory must be set before images can be downloaded").Code(http.StatusBadRequest) } + + if len(params.Devices) == 0 { + return errs.Wrapw(ErrNoDevices, "downloading images requires at least one device configured").Code(http.StatusBadRequest) + } + return nil } diff --git a/api/download_subreddit_posts.go b/api/download_subreddit_posts.go new file mode 100644 index 0000000..a83900f --- /dev/null +++ b/api/download_subreddit_posts.go @@ -0,0 +1,16 @@ +package api + +import ( + "context" + + "github.com/tigorlazuardi/redmage/api/reddit" +) + +type DownloadSubredditPostsParams struct { + Page int + Limit int +} + +func (api *API) DownloadSubredditPosts(ctx context.Context, subredditName string, params DownloadSubredditParams) (posts []reddit.Post, err error) { + return posts, err +} diff --git a/api/reddit/client.go b/api/reddit/client.go new file mode 100644 index 0000000..d05f45f --- /dev/null +++ b/api/reddit/client.go @@ -0,0 +1,9 @@ +package reddit + +import ( + "net/http" +) + +type Client interface { + Do(*http.Request) (*http.Response, error) +} diff --git a/api/reddit/fancy_reader.go b/api/reddit/fancy_reader.go new file mode 100644 index 0000000..abb1d89 --- /dev/null +++ b/api/reddit/fancy_reader.go @@ -0,0 +1 @@ +package reddit diff --git a/api/reddit/get_posts.go b/api/reddit/get_posts.go new file mode 100644 index 0000000..75c1ee8 --- /dev/null +++ b/api/reddit/get_posts.go @@ -0,0 +1,64 @@ +package reddit + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + + "github.com/tigorlazuardi/redmage/pkg/errs" +) + +type SubredditType int + +const ( + SubredditTypeSub SubredditType = iota + SubredditTypeUser +) + +func (s SubredditType) Code() string { + switch s { + case SubredditTypeUser: + return "user" + default: + return "r" + } +} + +type GetPostsParam struct { + Subreddit string + Limit int + Page int + SubredditType SubredditType +} + +func (reddit *Reddit) GetPosts(ctx context.Context, params GetPostsParam) (posts []Post, err error) { + url := fmt.Sprintf("https://reddit.com/%s/%s.json?limit=%d&page=%d", params.SubredditType.Code(), params.Subreddit, params.Limit, params.Page) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) + if err != nil { + return posts, errs.Wrapw(err, "reddit: failed to create http request instance", "url", url, "params", params) + } + + res, err := reddit.Client.Do(req) + if err != nil { + return posts, errs.Wrapw(err, "reddit: failed to execute http request", "url", url, "params", params) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + body, _ := io.ReadAll(res.Body) + return posts, errs.Fail("reddit: unexpected status code when executing GetPosts", + slog.Group("request", "url", url, "params", params), + slog.Group("response", "status_code", res.StatusCode, "body", json.RawMessage(body)), + ) + } + + err = json.NewDecoder(res.Body).Decode(&posts) + if err != nil { + return posts, errs.Wrapw(err, "reddit: failed to parse response body when getting posts from reddit", "url", url, "params", params) + } + + return posts, nil +} diff --git a/api/reddit/post.go b/api/reddit/post.go new file mode 100644 index 0000000..5366829 --- /dev/null +++ b/api/reddit/post.go @@ -0,0 +1,191 @@ +package reddit + +type Post struct { + Kind string `json:"kind"` + Data Data `json:"data"` +} + +type ( + MediaEmbed struct{} + SecureMediaEmbed struct{} + Gildings struct{} + Source struct { + URL string `json:"url"` + Width int `json:"width"` + Height int `json:"height"` + } +) + +type Resolutions struct { + URL string `json:"url"` + Width int `json:"width"` + Height int `json:"height"` +} +type ( + Variants struct{} + Images struct { + Source Source `json:"source"` + Resolutions []Resolutions `json:"resolutions"` + Variants Variants `json:"variants"` + ID string `json:"id"` + } +) + +type Preview struct { + Images []Images `json:"images"` + Enabled bool `json:"enabled"` +} +type LinkFlairRichtext struct { + E string `json:"e"` + T string `json:"t"` +} +type ThumbnailPreview struct { + Y int `json:"y"` + X int `json:"x"` + U string `json:"u"` +} + +type MediaMetadata struct { + Status string `json:"status"` + Kind string `json:"e"` + Mimetype string `json:"m"` + ExtraThumbnails []ThumbnailPreview `json:"p"` + Thumbnail ThumbnailPreview `json:"s"` + ID string `json:"id"` +} +type Items struct { + OutboundURL string `json:"outbound_url,omitempty"` + MediaID string `json:"media_id"` + ID int `json:"id"` +} +type GalleryData struct { + Items []Items `json:"items"` +} +type AuthorFlairRichtext struct { + E string `json:"e"` + T string `json:"t"` +} +type ChildrenData struct { + ApprovedAtUtc any `json:"approved_at_utc"` + Subreddit string `json:"subreddit"` + Selftext string `json:"selftext"` + AuthorFullname string `json:"author_fullname"` + Saved bool `json:"saved"` + ModReasonTitle any `json:"mod_reason_title"` + Gilded int `json:"gilded"` + Clicked bool `json:"clicked"` + IsGallery bool `json:"is_gallery"` + Title string `json:"title"` + LinkFlairRichtext []LinkFlairRichtext `json:"link_flair_richtext"` + SubredditNamePrefixed string `json:"subreddit_name_prefixed"` + Hidden bool `json:"hidden"` + Pwls int `json:"pwls"` + LinkFlairCSSClass string `json:"link_flair_css_class"` + Downs int `json:"downs"` + ThumbnailHeight int `json:"thumbnail_height"` + TopAwardedType any `json:"top_awarded_type"` + HideScore bool `json:"hide_score"` + MediaMetadata map[string]MediaMetadata `json:"media_metadata"` + Name string `json:"name"` + Quarantine bool `json:"quarantine"` + LinkFlairTextColor any `json:"link_flair_text_color"` + UpvoteRatio float64 `json:"upvote_ratio"` + AuthorFlairBackgroundColor any `json:"author_flair_background_color"` + Ups int `json:"ups"` + Domain string `json:"domain"` + MediaEmbed MediaEmbed `json:"media_embed"` + ThumbnailWidth int `json:"thumbnail_width"` + AuthorFlairTemplateID string `json:"author_flair_template_id"` + IsOriginalContent bool `json:"is_original_content"` + UserReports []any `json:"user_reports"` + SecureMedia any `json:"secure_media"` + IsRedditMediaDomain bool `json:"is_reddit_media_domain"` + IsMeta bool `json:"is_meta"` + Category any `json:"category"` + SecureMediaEmbed SecureMediaEmbed `json:"secure_media_embed"` + GalleryData GalleryData `json:"gallery_data"` + LinkFlairText string `json:"link_flair_text"` + CanModPost bool `json:"can_mod_post"` + Score int `json:"score"` + ApprovedBy any `json:"approved_by"` + IsCreatedFromAdsUI bool `json:"is_created_from_ads_ui"` + AuthorPremium bool `json:"author_premium"` + Thumbnail string `json:"thumbnail"` + Edited bool `json:"edited"` + AuthorFlairCSSClass string `json:"author_flair_css_class"` + AuthorFlairRichtext []AuthorFlairRichtext `json:"author_flair_richtext"` + Gildings Gildings `json:"gildings"` + ContentCategories any `json:"content_categories"` + IsSelf bool `json:"is_self"` + SubredditType string `json:"subreddit_type"` + Created int `json:"created"` + LinkFlairType string `json:"link_flair_type"` + Wls int `json:"wls"` + RemovedByCategory any `json:"removed_by_category"` + BannedBy any `json:"banned_by"` + AuthorFlairType string `json:"author_flair_type"` + TotalAwardsReceived int `json:"total_awards_received"` + AllowLiveComments bool `json:"allow_live_comments"` + SelftextHTML any `json:"selftext_html"` + Likes any `json:"likes"` + SuggestedSort any `json:"suggested_sort"` + BannedAtUtc any `json:"banned_at_utc"` + URLOverriddenByDest string `json:"url_overridden_by_dest"` + ViewCount any `json:"view_count"` + Archived bool `json:"archived"` + NoFollow bool `json:"no_follow"` + IsCrosspostable bool `json:"is_crosspostable"` + Pinned bool `json:"pinned"` + Over18 bool `json:"over_18"` + AllAwardings []any `json:"all_awardings"` + Awarders []any `json:"awarders"` + MediaOnly bool `json:"media_only"` + CanGild bool `json:"can_gild"` + Spoiler bool `json:"spoiler"` + Locked bool `json:"locked"` + AuthorFlairText string `json:"author_flair_text"` + TreatmentTags []any `json:"treatment_tags"` + Visited bool `json:"visited"` + RemovedBy any `json:"removed_by"` + ModNote any `json:"mod_note"` + Distinguished any `json:"distinguished"` + SubredditID string `json:"subreddit_id"` + AuthorIsBlocked bool `json:"author_is_blocked"` + ModReasonBy any `json:"mod_reason_by"` + NumReports any `json:"num_reports"` + RemovalReason any `json:"removal_reason"` + LinkFlairBackgroundColor any `json:"link_flair_background_color"` + ID string `json:"id"` + IsRobotIndexable bool `json:"is_robot_indexable"` + ReportReasons any `json:"report_reasons"` + Author string `json:"author"` + DiscussionType any `json:"discussion_type"` + NumComments int `json:"num_comments"` + SendReplies bool `json:"send_replies"` + WhitelistStatus string `json:"whitelist_status"` + ContestMode bool `json:"contest_mode"` + ModReports []any `json:"mod_reports"` + AuthorPatreonFlair bool `json:"author_patreon_flair"` + AuthorFlairTextColor string `json:"author_flair_text_color"` + Permalink string `json:"permalink"` + ParentWhitelistStatus string `json:"parent_whitelist_status"` + Stickied bool `json:"stickied"` + URL string `json:"url"` + SubredditSubscribers int `json:"subreddit_subscribers"` + CreatedUtc int `json:"created_utc"` + NumCrossposts int `json:"num_crossposts"` + Media any `json:"media"` + IsVideo bool `json:"is_video"` +} +type Children struct { + Kind string `json:"kind"` + Data ChildrenData `json:"data,omitempty"` +} +type Data struct { + After string `json:"after"` + Dist int `json:"dist"` + Modhash string `json:"modhash"` + GeoFilter any `json:"geo_filter"` + Children []Children `json:"children"` + Before any `json:"before"` +} diff --git a/api/reddit/reddit.go b/api/reddit/reddit.go new file mode 100644 index 0000000..168c086 --- /dev/null +++ b/api/reddit/reddit.go @@ -0,0 +1,8 @@ +package reddit + +import "github.com/tigorlazuardi/redmage/config" + +type Reddit struct { + Client Client + Config *config.Config +} diff --git a/api/subredditpost/subredditpost.go b/api/subredditpost/subredditpost.go new file mode 100644 index 0000000..131022c --- /dev/null +++ b/api/subredditpost/subredditpost.go @@ -0,0 +1,191 @@ +package subredditpost + +type SubredditPost struct { + Kind string `json:"kind"` + Data Data `json:"data"` +} + +type ( + MediaEmbed struct{} + SecureMediaEmbed struct{} + Gildings struct{} + Source struct { + URL string `json:"url"` + Width int `json:"width"` + Height int `json:"height"` + } +) + +type Resolutions struct { + URL string `json:"url"` + Width int `json:"width"` + Height int `json:"height"` +} +type ( + Variants struct{} + Images struct { + Source Source `json:"source"` + Resolutions []Resolutions `json:"resolutions"` + Variants Variants `json:"variants"` + ID string `json:"id"` + } +) + +type Preview struct { + Images []Images `json:"images"` + Enabled bool `json:"enabled"` +} +type LinkFlairRichtext struct { + E string `json:"e"` + T string `json:"t"` +} +type ThumbnailPreview struct { + Y int `json:"y"` + X int `json:"x"` + U string `json:"u"` +} + +type MediaMetadata struct { + Status string `json:"status"` + Kind string `json:"e"` + Mimetype string `json:"m"` + ExtraThumbnails []ThumbnailPreview `json:"p"` + Thumbnail ThumbnailPreview `json:"s"` + ID string `json:"id"` +} +type Items struct { + OutboundURL string `json:"outbound_url,omitempty"` + MediaID string `json:"media_id"` + ID int `json:"id"` +} +type GalleryData struct { + Items []Items `json:"items"` +} +type AuthorFlairRichtext struct { + E string `json:"e"` + T string `json:"t"` +} +type ChildrenData struct { + ApprovedAtUtc any `json:"approved_at_utc"` + Subreddit string `json:"subreddit"` + Selftext string `json:"selftext"` + AuthorFullname string `json:"author_fullname"` + Saved bool `json:"saved"` + ModReasonTitle any `json:"mod_reason_title"` + Gilded int `json:"gilded"` + Clicked bool `json:"clicked"` + IsGallery bool `json:"is_gallery"` + Title string `json:"title"` + LinkFlairRichtext []LinkFlairRichtext `json:"link_flair_richtext"` + SubredditNamePrefixed string `json:"subreddit_name_prefixed"` + Hidden bool `json:"hidden"` + Pwls int `json:"pwls"` + LinkFlairCSSClass string `json:"link_flair_css_class"` + Downs int `json:"downs"` + ThumbnailHeight int `json:"thumbnail_height"` + TopAwardedType any `json:"top_awarded_type"` + HideScore bool `json:"hide_score"` + MediaMetadata map[string]MediaMetadata `json:"media_metadata"` + Name string `json:"name"` + Quarantine bool `json:"quarantine"` + LinkFlairTextColor any `json:"link_flair_text_color"` + UpvoteRatio float64 `json:"upvote_ratio"` + AuthorFlairBackgroundColor any `json:"author_flair_background_color"` + Ups int `json:"ups"` + Domain string `json:"domain"` + MediaEmbed MediaEmbed `json:"media_embed"` + ThumbnailWidth int `json:"thumbnail_width"` + AuthorFlairTemplateID string `json:"author_flair_template_id"` + IsOriginalContent bool `json:"is_original_content"` + UserReports []any `json:"user_reports"` + SecureMedia any `json:"secure_media"` + IsRedditMediaDomain bool `json:"is_reddit_media_domain"` + IsMeta bool `json:"is_meta"` + Category any `json:"category"` + SecureMediaEmbed SecureMediaEmbed `json:"secure_media_embed"` + GalleryData GalleryData `json:"gallery_data"` + LinkFlairText string `json:"link_flair_text"` + CanModPost bool `json:"can_mod_post"` + Score int `json:"score"` + ApprovedBy any `json:"approved_by"` + IsCreatedFromAdsUI bool `json:"is_created_from_ads_ui"` + AuthorPremium bool `json:"author_premium"` + Thumbnail string `json:"thumbnail"` + Edited bool `json:"edited"` + AuthorFlairCSSClass string `json:"author_flair_css_class"` + AuthorFlairRichtext []AuthorFlairRichtext `json:"author_flair_richtext"` + Gildings Gildings `json:"gildings"` + ContentCategories any `json:"content_categories"` + IsSelf bool `json:"is_self"` + SubredditType string `json:"subreddit_type"` + Created int `json:"created"` + LinkFlairType string `json:"link_flair_type"` + Wls int `json:"wls"` + RemovedByCategory any `json:"removed_by_category"` + BannedBy any `json:"banned_by"` + AuthorFlairType string `json:"author_flair_type"` + TotalAwardsReceived int `json:"total_awards_received"` + AllowLiveComments bool `json:"allow_live_comments"` + SelftextHTML any `json:"selftext_html"` + Likes any `json:"likes"` + SuggestedSort any `json:"suggested_sort"` + BannedAtUtc any `json:"banned_at_utc"` + URLOverriddenByDest string `json:"url_overridden_by_dest"` + ViewCount any `json:"view_count"` + Archived bool `json:"archived"` + NoFollow bool `json:"no_follow"` + IsCrosspostable bool `json:"is_crosspostable"` + Pinned bool `json:"pinned"` + Over18 bool `json:"over_18"` + AllAwardings []any `json:"all_awardings"` + Awarders []any `json:"awarders"` + MediaOnly bool `json:"media_only"` + CanGild bool `json:"can_gild"` + Spoiler bool `json:"spoiler"` + Locked bool `json:"locked"` + AuthorFlairText string `json:"author_flair_text"` + TreatmentTags []any `json:"treatment_tags"` + Visited bool `json:"visited"` + RemovedBy any `json:"removed_by"` + ModNote any `json:"mod_note"` + Distinguished any `json:"distinguished"` + SubredditID string `json:"subreddit_id"` + AuthorIsBlocked bool `json:"author_is_blocked"` + ModReasonBy any `json:"mod_reason_by"` + NumReports any `json:"num_reports"` + RemovalReason any `json:"removal_reason"` + LinkFlairBackgroundColor any `json:"link_flair_background_color"` + ID string `json:"id"` + IsRobotIndexable bool `json:"is_robot_indexable"` + ReportReasons any `json:"report_reasons"` + Author string `json:"author"` + DiscussionType any `json:"discussion_type"` + NumComments int `json:"num_comments"` + SendReplies bool `json:"send_replies"` + WhitelistStatus string `json:"whitelist_status"` + ContestMode bool `json:"contest_mode"` + ModReports []any `json:"mod_reports"` + AuthorPatreonFlair bool `json:"author_patreon_flair"` + AuthorFlairTextColor string `json:"author_flair_text_color"` + Permalink string `json:"permalink"` + ParentWhitelistStatus string `json:"parent_whitelist_status"` + Stickied bool `json:"stickied"` + URL string `json:"url"` + SubredditSubscribers int `json:"subreddit_subscribers"` + CreatedUtc int `json:"created_utc"` + NumCrossposts int `json:"num_crossposts"` + Media any `json:"media"` + IsVideo bool `json:"is_video"` +} +type Children struct { + Kind string `json:"kind"` + Data ChildrenData `json:"data,omitempty"` +} +type Data struct { + After string `json:"after"` + Dist int `json:"dist"` + Modhash string `json:"modhash"` + GeoFilter any `json:"geo_filter"` + Children []Children `json:"children"` + Before any `json:"before"` +} diff --git a/pkg/errs/errs.go b/pkg/errs/errs.go index 5b91db7..2f630b5 100644 --- a/pkg/errs/errs.go +++ b/pkg/errs/errs.go @@ -102,6 +102,10 @@ func (er *Err) Error() string { return s.String() } +func (er *Err) Unwrap() error { + return er.origin +} + func (er *Err) Message(msg string, args ...any) Error { er.message = fmt.Sprintf(msg, args...) return er