diff --git a/README.md b/README.md index e619353..86ace18 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,13 @@ Geddit is a convenient abstraction for the [reddit.com](http://reddit.com) API i This library is a WIP. It should have some API coverage, but does not yet include things like the new OAuth model. -## example +## examples + +See [godoc](http://godoc.org/github.com/jzelinskie/geddit) for OAuth examples. + +Here is an example usage of the old, cookie authentication method: + +(NOTE: You will be heavily rate-limited by reddit's API when using cookies. Consider switching to OAuth). ```Go package main diff --git a/apponlyoauth_session.go b/apponlyoauth_session.go new file mode 100644 index 0000000..ae1e7c9 --- /dev/null +++ b/apponlyoauth_session.go @@ -0,0 +1,267 @@ +// Copyright 2012 Jimmy Zelinskie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Copyright 2016 Samir Bhatt. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package geddit implements an abstraction for the reddit.com API. +package geddit + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "time" + + "github.com/google/go-querystring/query" + "golang.org/x/net/context" + "golang.org/x/oauth2/clientcredentials" +) + +// AppOnlyOAuthSession represents an OAuth session with reddit.com -- +// all authenticated API calls are methods bound to this type. +type AppOnlyOAuthSession struct { + Client *http.Client + ClientID string + ClientSecret string + OAuthConfig *clientcredentials.Config + TokenExpiry time.Time + UserAgent string + ctx context.Context + Debug bool +} + +// NewAppOnlyOAuthSession creates a new session for those who want to log into a +// reddit account via Application Only OAuth. +// See https://github.com/reddit/reddit/wiki/OAuth2#application-only-oauth +func NewAppOnlyOAuthSession(clientID, clientSecret, useragent string, debug bool) (*AppOnlyOAuthSession, error) { + s := &AppOnlyOAuthSession{} + + if useragent != "" { + s.UserAgent = useragent + } else { + s.UserAgent = "Geddit API Client https://github.com/imheresamir/geddit" + } + + s.ClientID = clientID + s.ClientSecret = clientSecret + + // Set OAuth config + s.OAuthConfig = &clientcredentials.Config{ + ClientID: clientID, + ClientSecret: clientSecret, + TokenURL: "https://www.reddit.com/api/v1/access_token", + } + + s.ctx = context.Background() + + return s, nil +} + +// refreshToken should be called internally before each API call +func (a *AppOnlyOAuthSession) refreshToken() error { + // Check if token needs to be refreshed + if time.Now().Before(a.TokenExpiry) { + return nil + } + + // Fetch OAuth token + t, err := a.OAuthConfig.Token(a.ctx) + if err != nil { + return err + } + a.TokenExpiry = t.Expiry + + a.Client = a.OAuthConfig.Client(a.ctx) + return nil +} + +func (a *AppOnlyOAuthSession) getBody(link string, d interface{}) error { + a.refreshToken() + + req, err := http.NewRequest("GET", link, nil) + if err != nil { + return err + } + + // This is needed to avoid rate limits + req.Header.Set("User-Agent", a.UserAgent) + + if a.Client == nil { + return errors.New("OAuth Session lacks HTTP client! Error getting token") + } + + resp, err := a.Client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + + // DEBUG + if a.Debug { + fmt.Printf("***DEBUG***\nRequest Body: %s\n***DEBUG***\n\n", body) + } + + err = json.Unmarshal(body, d) + if err != nil { + return err + } + + return nil +} + +// Listing returns a slice of Submission pointers. +// See https://www.reddit.com/dev/api#listings for documentation. +func (a *AppOnlyOAuthSession) Listing(username, listing string, sort popularitySort, params ListingOptions) ([]*Submission, error) { + p, err := query.Values(params) + if err != nil { + return nil, err + } + if sort != "" { + p.Set("sort", string(sort)) + } + + type resp struct { + Data struct { + Children []struct { + Data *Submission + } + } + } + r := &resp{} + url := fmt.Sprintf("https://oauth.reddit.com/user/%s/%s?%s", username, listing, p.Encode()) + err = a.getBody(url, r) + if err != nil { + return nil, err + } + + submissions := make([]*Submission, len(r.Data.Children)) + for i, child := range r.Data.Children { + submissions[i] = child.Data + } + + return submissions, nil +} + +func (a *AppOnlyOAuthSession) Upvoted(username string, sort popularitySort, params ListingOptions) ([]*Submission, error) { + return a.Listing(username, "upvoted", sort, params) +} + +// AboutRedditor returns a Redditor for the given username using OAuth. +func (a *AppOnlyOAuthSession) AboutRedditor(user string) (*Redditor, error) { + type redditor struct { + Data Redditor + } + r := &redditor{} + link := fmt.Sprintf("https://oauth.reddit.com/user/%s/about", user) + + err := a.getBody(link, r) + if err != nil { + return nil, err + } + return &r.Data, nil +} + +func (a *AppOnlyOAuthSession) UserTrophies(user string) ([]*Trophy, error) { + type trophyData struct { + Data struct { + Trophies []struct { + Data Trophy + } + } + } + + t := &trophyData{} + url := fmt.Sprintf("https://oauth.reddit.com/api/v1/user/%s/trophies", user) + err := a.getBody(url, t) + if err != nil { + return nil, err + } + + var trophies []*Trophy + for _, trophy := range t.Data.Trophies { + trophies = append(trophies, &trophy.Data) + } + return trophies, nil +} + +// AboutSubreddit returns a subreddit for the given subreddit name using OAuth. +func (a *AppOnlyOAuthSession) AboutSubreddit(name string) (*Subreddit, error) { + type subreddit struct { + Data Subreddit + } + sr := &subreddit{} + link := fmt.Sprintf("https://oauth.reddit.com/r/%s/about", name) + + err := a.getBody(link, sr) + if err != nil { + return nil, err + } + return &sr.Data, nil +} + +// Comments returns the comments for a given Submission using OAuth. +func (a *AppOnlyOAuthSession) Comments(h *Submission, sort popularitySort, params ListingOptions) ([]*Comment, error) { + p, err := query.Values(params) + if err != nil { + return nil, err + } + var c interface{} + link := fmt.Sprintf("https://oauth.reddit.com/comments/%s?%s", h.ID, p.Encode()) + err = a.getBody(link, &c) + if err != nil { + return nil, err + } + helper := new(helper) + helper.buildComments(c) + return helper.comments, nil +} + +// SubredditSubmissions returns the submissions on the given subreddit using OAuth. +func (a *AppOnlyOAuthSession) SubredditSubmissions(subreddit string, sort popularitySort, params ListingOptions) ([]*Submission, error) { + v, err := query.Values(params) + if err != nil { + return nil, err + } + + baseUrl := "https://oauth.reddit.com" + + // If subbreddit given, add to URL + if subreddit != "" { + baseUrl += "/r/" + subreddit + } + + redditURL := fmt.Sprintf(baseUrl+"/%s.json?%s", sort, v.Encode()) + + type Response struct { + Data struct { + Children []struct { + Data *Submission + } + } + } + + r := new(Response) + err = a.getBody(redditURL, r) + if err != nil { + return nil, err + } + + submissions := make([]*Submission, len(r.Data.Children)) + for i, child := range r.Data.Children { + submissions[i] = child.Data + } + + return submissions, nil +} + +// Frontpage returns the submissions on the default reddit frontpage using OAuth. +func (a *AppOnlyOAuthSession) Frontpage(sort popularitySort, params ListingOptions) ([]*Submission, error) { + return a.SubredditSubmissions("", sort, params) +} diff --git a/apponlyoauth_session_test.go b/apponlyoauth_session_test.go new file mode 100644 index 0000000..e82866c --- /dev/null +++ b/apponlyoauth_session_test.go @@ -0,0 +1,35 @@ +// Copyright 2012 Jimmy Zelinskie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Copyright 2016 Samir Bhatt. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package geddit + +import ( + "testing" +) + +// TODO: Write better test functions + +func TestMain(t *testing.T) { + a, err := NewAppOnlyOAuthSession( + "client_id", + "client_secret", + "Testing OAuth Bot by u/imheresamir v0.1 see source https://github.com/imheresamir/geddit", + false, + ) + if err != nil { + t.Fatal(err) + } + + // Ready to make API calls! + _, err = a.SubredditSubmissions("hiphopheads", "hot", ListingOptions{}) + + if err != nil { + t.Fatal(err) + } + +} diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..80024df --- /dev/null +++ b/example_test.go @@ -0,0 +1,61 @@ +// Copyright 2012 Jimmy Zelinskie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package geddit_test + +import ( + "fmt" + "log" + + "github.com/jzelinskie/geddit" +) + +func ExampleNewOAuthSession_login() { + o, err := geddit.NewOAuthSession( + "client_id", + "client_secret", + "Testing OAuth Bot by u/my_user v0.1 see source https://github.com/jzelinskie/geddit", + "http://redirect.url", + ) + if err != nil { + log.Fatal(err) + } + + // Create new auth token for confidential clients (personal scripts/apps). + err = o.LoginAuth("my_user", "my_password") + if err != nil { + log.Fatal(err) + } + + // Ready to make API calls! +} + +func ExampleNewOAuthSession_url() { + o, err := geddit.NewOAuthSession( + "client_id", + "client_secret", + "Testing OAuth Bot by u/my_user v0.1 see source https://github.com/jzelinskie/geddit", + "http://redirect.url", + ) + if err != nil { + log.Fatal(err) + } + + // Pass a random/unique state string which will be returned to the + // redirect URL. Ideally, you should verify that it matches to + // avoid CSRF attack. + url := o.AuthCodeURL("random string", []string{"indentity", "read"}) + fmt.Printf("Visit %s to obtain auth code", url) + + var code string + fmt.Scanln(&code) + + // Create and set token using given auth code. + err = o.CodeAuth(code) + if err != nil { + log.Fatal(err) + } + + // Ready to make API calls! +} diff --git a/oauth_session.go b/oauth_session.go new file mode 100644 index 0000000..fd9d264 --- /dev/null +++ b/oauth_session.go @@ -0,0 +1,621 @@ +// Copyright 2012 Jimmy Zelinskie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package reddit implements an abstraction for the reddit.com API. +package geddit + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/beefsack/go-rate" + "github.com/google/go-querystring/query" + "golang.org/x/net/context" + "golang.org/x/oauth2" +) + +// OAuthSession represents an OAuth session with reddit.com -- +// all authenticated API calls are methods bound to this type. +type OAuthSession struct { + Client *http.Client + ClientID string + ClientSecret string + OAuthConfig *oauth2.Config + TokenExpiry time.Time + UserAgent string + ctx context.Context + throttle *rate.RateLimiter + debug bool +} + +// NewLoginSession creates a new session for those who want to log into a +// reddit account via OAuth. +func NewOAuthSession(clientID, clientSecret, useragent, redirectURL string, debug bool) (*OAuthSession, error) { + s := &OAuthSession{ + ClientID: clientID, + ClientSecret: clientSecret, + debug: debug, + } + + if useragent != "" { + s.UserAgent = useragent + } else { + s.UserAgent = "Geddit Reddit Bot https://github.com/jzelinskie/geddit" + } + + // Set OAuth config + s.OAuthConfig = &oauth2.Config{ + ClientID: clientID, + ClientSecret: clientSecret, + Endpoint: oauth2.Endpoint{ + AuthURL: "https://www.reddit.com/api/v1/authorize", + TokenURL: "https://www.reddit.com/api/v1/access_token", + }, + RedirectURL: redirectURL, + } + s.ctx = context.Background() + return s, nil +} + +// Throttle sets the interval of each HTTP request. +// Disable by setting interval to 0. Disabled by default. +// Throttling is applied to invidual OAuthSession types. +func (o *OAuthSession) Throttle(interval time.Duration) { + if interval == 0 { + o.throttle = nil + return + } + o.throttle = rate.New(1, interval) +} + +// LoginAuth creates the required HTTP client with a new token. +func (o *OAuthSession) LoginAuth(username, password string) error { + // Fetch OAuth token. + t, err := o.OAuthConfig.PasswordCredentialsToken(o.ctx, username, password) + if err != nil { + return err + } + o.TokenExpiry = t.Expiry + o.Client = o.OAuthConfig.Client(o.ctx, t) + return nil +} + +// AuthCodeURL creates and returns an auth URL which contains an auth code. +func (o *OAuthSession) AuthCodeURL(state string, scopes []string, duration string) string { + o.OAuthConfig.Scopes = scopes + return o.OAuthConfig.AuthCodeURL(state, oauth2.AccessTypeOnline, oauth2.SetAuthURLParam("duration", duration)) +} + +// CodeAuth creates and sets a token using an authentication code returned from AuthCodeURL. +func (o *OAuthSession) CodeAuth(code string) error { + t, err := o.OAuthConfig.Exchange(o.ctx, code) + if err != nil { + return err + } + o.TokenExpiry = t.Expiry + o.Client = o.OAuthConfig.Client(context.Background(), t) + return nil +} + +// NeedsCaptcha check whether CAPTCHAs are needed for the Submit function. +func (o *OAuthSession) NeedsCaptcha() (bool, error) { + var b bool + err := o.getBody("https://oauth.reddit.com/api/needs_captcha", &b) + if err != nil { + return false, err + } + return b, nil +} + +// NewCaptcha returns a string used to create CAPTCHA links for users. +func (o *OAuthSession) NewCaptcha() (string, error) { + // Build form for POST request. + v := url.Values{ + "api_type": {"json"}, + } + + type captcha struct { + Json struct { + Errors [][]string + Data struct { + Iden string + } + } + } + c := &captcha{} + + err := o.postBody("https://oauth.reddit.com/api/new_captcha", v, c) + if err != nil { + return "", err + } + return c.Json.Data.Iden, nil +} + +func (o *OAuthSession) getBody(link string, d interface{}) error { + req, err := http.NewRequest("GET", link, nil) + if err != nil { + return err + } + + // This is needed to avoid rate limits + req.Header.Set("User-Agent", o.UserAgent) + + if o.Client == nil { + return errors.New("OAuth Session lacks HTTP client! Use func (o OAuthSession) LoginAuth() to make one.") + } + + // Throttle request + if o.throttle != nil { + o.throttle.Wait() + } + + resp, err := o.Client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + + // DEBUG + if o.debug { + fmt.Printf("***DEBUG***\nRequest Body: %s\n***DEBUG***\n\n", body) + } + + err = json.Unmarshal(body, d) + if err != nil { + return err + } + + return nil +} + +func (o *OAuthSession) Me() (*Redditor, error) { + r := &Redditor{} + err := o.getBody("https://oauth.reddit.com/api/v1/me", r) + if err != nil { + return nil, err + } + return r, nil +} + +func (o *OAuthSession) MyKarma() ([]Karma, error) { + type karma struct { + Data []Karma + } + k := &karma{} + err := o.getBody("https://oauth.reddit.com/api/v1/me/karma", k) + if err != nil { + return nil, err + } + return k.Data, nil +} + +func (o *OAuthSession) MyPreferences() (*Preferences, error) { + p := &Preferences{} + err := o.getBody("https://oauth.reddit.com/api/v1/me/prefs", p) + if err != nil { + return nil, err + } + return p, nil +} + +func (o *OAuthSession) MyFriends() ([]Friend, error) { + type friends struct { + Data struct { + Children []Friend + } + } + f := &friends{} + err := o.getBody("https://oauth.reddit.com/api/v1/me/friends", f) + if err != nil { + return nil, err + } + return f.Data.Children, nil +} + +func (o *OAuthSession) MyTrophies() ([]*Trophy, error) { + type trophyData struct { + Data struct { + Trophies []struct { + Data Trophy + } + } + } + + t := &trophyData{} + err := o.getBody("https://oauth.reddit.com/api/v1/me/trophies", t) + if err != nil { + return nil, err + } + + var myTrophies []*Trophy + for _, trophy := range t.Data.Trophies { + myTrophies = append(myTrophies, &trophy.Data) + } + return myTrophies, nil +} + +// Listing returns a slice of Submission pointers. +// See https://www.reddit.com/dev/api#listings for documentation. +func (o *OAuthSession) Listing(username, listing string, sort popularitySort, params ListingOptions) ([]*Submission, error) { + p, err := query.Values(params) + if err != nil { + return nil, err + } + if sort != "" { + p.Set("sort", string(sort)) + } + + type resp struct { + Data struct { + Children []struct { + Data *Submission + } + } + } + r := &resp{} + url := fmt.Sprintf("https://oauth.reddit.com/user/%s/%s?%s", username, listing, p.Encode()) + err = o.getBody(url, r) + if err != nil { + return nil, err + } + + submissions := make([]*Submission, len(r.Data.Children)) + for i, child := range r.Data.Children { + submissions[i] = child.Data + } + + return submissions, nil +} + +func (o *OAuthSession) Upvoted(username string, sort popularitySort, params ListingOptions) ([]*Submission, error) { + return o.Listing(username, "upvoted", sort, params) +} + +func (o *OAuthSession) MyUpvoted(sort popularitySort, params ListingOptions) ([]*Submission, error) { + me, err := o.Me() + if err != nil { + return nil, err + } + return o.Listing(me.Name, "upvoted", sort, params) +} + +// AboutRedditor returns a Redditor for the given username using OAuth. +func (o *OAuthSession) AboutRedditor(user string) (*Redditor, error) { + type redditor struct { + Data Redditor + } + r := &redditor{} + link := fmt.Sprintf("https://oauth.reddit.com/user/%s/about", user) + + err := o.getBody(link, r) + if err != nil { + return nil, err + } + return &r.Data, nil +} + +func (o *OAuthSession) UserTrophies(user string) ([]*Trophy, error) { + type trophyData struct { + Data struct { + Trophies []struct { + Data Trophy + } + } + } + + t := &trophyData{} + url := fmt.Sprintf("https://oauth.reddit.com/api/v1/user/%s/trophies", user) + err := o.getBody(url, t) + if err != nil { + return nil, err + } + + var trophies []*Trophy + for _, trophy := range t.Data.Trophies { + trophies = append(trophies, &trophy.Data) + } + return trophies, nil +} + +// AboutSubreddit returns a subreddit for the given subreddit name using OAuth. +func (o *OAuthSession) AboutSubreddit(name string) (*Subreddit, error) { + type subreddit struct { + Data Subreddit + } + sr := &subreddit{} + link := fmt.Sprintf("https://oauth.reddit.com/r/%s/about", name) + + err := o.getBody(link, sr) + if err != nil { + return nil, err + } + return &sr.Data, nil +} + +// Comments returns the comments for a given Submission using OAuth. +func (o *OAuthSession) Comments(h *Submission, sort popularitySort, params ListingOptions) ([]*Comment, error) { + p, err := query.Values(params) + if err != nil { + return nil, err + } + var c interface{} + link := fmt.Sprintf("https://oauth.reddit.com/comments/%s?%s", h.ID, p.Encode()) + err = o.getBody(link, &c) + if err != nil { + return nil, err + } + helper := new(helper) + helper.buildComments(c) + return helper.comments, nil +} + +func (o *OAuthSession) postBody(link string, form url.Values, d interface{}) error { + req, err := http.NewRequest("POST", link, strings.NewReader(form.Encode())) + if err != nil { + return err + } + + // This is needed to avoid rate limits + req.Header.Set("User-Agent", o.UserAgent) + + // POST form provided + req.PostForm = form + + if o.Client == nil { + return errors.New("OAuth Session lacks HTTP client! Use func (o OAuthSession) LoginAuth() to make one.") + } + + // Throttle request + if o.throttle != nil { + o.throttle.Wait() + } + + resp, err := o.Client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + + // DEBUG + if o.debug { + fmt.Printf("***DEBUG***\nRequest Body: %s\n***DEBUG***\n\n", body) + } + + // The caller may want JSON decoded, or this could just be an update/delete request. + if d != nil { + err = json.Unmarshal(body, d) + if err != nil { + return err + } + } + + return nil +} + +// Submit accepts a NewSubmission type and submits a new link using OAuth. +// Returns a Submission type. +func (o *OAuthSession) Submit(ns *NewSubmission) (*Submission, error) { + + // Build form for POST request. + v := url.Values{ + "title": {ns.Title}, + "url": {ns.Content}, + "text": {ns.Content}, + "sr": {ns.Subreddit}, + "sendreplies": {strconv.FormatBool(ns.SendReplies)}, + "resubmit": {strconv.FormatBool(ns.Resubmit)}, + "api_type": {"json"}, + // TODO implement captchas for OAuth types + //"captcha": {ns.Captcha.Response}, + //"iden": {ns.Captcha.Iden}, + } + if ns.Self { + v.Add("kind", "self") + } else { + v.Add("kind", "link") + } + + type submission struct { + Json struct { + Errors [][]string + Data Submission + } + } + submit := &submission{} + + err := o.postBody("https://oauth.reddit.com/api/submit", v, submit) + if err != nil { + return nil, err + } + // TODO check s.Errors and do something useful? + return &submit.Json.Data, nil +} + +// Delete deletes a link or comment using the given full name ID. +func (o *OAuthSession) Delete(d Deleter) error { + // Build form for POST request. + v := url.Values{} + v.Add("id", d.deleteID()) + + return o.postBody("https://oauth.reddit.com/api/del", v, nil) +} + +// SubredditSubmissions returns the submissions on the given subreddit using OAuth. +func (o *OAuthSession) SubredditSubmissions(subreddit string, sort popularitySort, params ListingOptions) ([]*Submission, error) { + v, err := query.Values(params) + if err != nil { + return nil, err + } + + baseUrl := "https://oauth.reddit.com" + + // If subbreddit given, add to URL + if subreddit != "" { + baseUrl += "/r/" + subreddit + } + + redditURL := fmt.Sprintf(baseUrl+"/%s.json?%s", sort, v.Encode()) + + type Response struct { + Data struct { + Children []struct { + Data *Submission + } + } + } + + r := new(Response) + err = o.getBody(redditURL, r) + if err != nil { + return nil, err + } + + submissions := make([]*Submission, len(r.Data.Children)) + for i, child := range r.Data.Children { + submissions[i] = child.Data + } + + return submissions, nil +} + +// Frontpage returns the submissions on the default reddit frontpage using OAuth. +func (o *OAuthSession) Frontpage(sort popularitySort, params ListingOptions) ([]*Submission, error) { + return o.SubredditSubmissions("", sort, params) +} + +// Vote either votes or rescinds a vote for a Submission or Comment using OAuth. +func (o *OAuthSession) Vote(v Voter, dir Vote) error { + // Build form for POST request. + form := url.Values{ + "id": {v.voteID()}, + "dir": {string(dir)}, + } + var vo interface{} + + err := o.postBody("https://oauth.reddit.com/api/vote", form, vo) + if err != nil { + return err + } + return nil +} + +// Reply posts a comment as a response to a Submission or Comment using OAuth. +func (o OAuthSession) Reply(r Replier, comment string) (*Comment, error) { + // Build form for POST request. + form := url.Values{ + "api_type": {"json"}, + "thing_id": {r.replyID()}, + "text": {comment}, + } + + type response struct { + JSON struct { + Errors [][]string + Data struct { + Things []struct { + Data map[string]interface{} + } + } + } + } + + res := &response{} + + err := o.postBody("https://oauth.reddit.com/api/comment", form, res) + if err != nil { + return nil, err + } + + if len(res.JSON.Errors) != 0 { + var msg []string + for _, k := range res.JSON.Errors { + msg = append(msg, k[1]) + } + return nil, errors.New(strings.Join(msg, ", ")) + } + + c := makeComment(res.JSON.Data.Things[0].Data) + + return c, nil +} + +// Save saves a link or comment using OAuth. +func (o *OAuthSession) Save(v Voter, category string) error { + // Build form for POST request. + form := url.Values{ + "id": {v.voteID()}, + "category": {category}, + } + var s interface{} + + err := o.postBody("https://oauth.reddit.com/api/save", form, s) + if err != nil { + return err + } + return nil +} + +// Unsave saves a link or comment using OAuth. +func (o *OAuthSession) Unsave(v Voter, category string) error { + // Build form for POST request. + form := url.Values{ + "id": {v.voteID()}, + "category": {category}, + } + var u interface{} + + err := o.postBody("https://oauth.reddit.com/api/unsave", form, u) + if err != nil { + return err + } + return nil +} + +// SavedLinks fetches links saved by given username using OAuth. +func (o *OAuthSession) SavedLinks(username string, params ListingOptions) ([]*Submission, error) { + return o.Listing(username, "saved", "", params) +} + +// MySavedLinks fetches links saved by current user using OAuth. +func (o *OAuthSession) MySavedLinks(params ListingOptions) ([]*Submission, error) { + me, err := o.Me() + if err != nil { + return nil, err + } + return o.Listing(me.Name, "saved", "", params) +} + +// SavedComments fetches comments saved by given username using OAuth. +func (o *OAuthSession) SavedComments(user string, params ListingOptions) ([]*Comment, error) { + var s interface{} + url := fmt.Sprintf("https://oauth.reddit.com/user/%s/saved", user) + err := o.getBody(url, &s) + if err != nil { + return nil, err + } + + helper := new(helper) + helper.buildComments(s) + return helper.comments, nil +} + +// MySavedComments fetches comments saved by current user using OAuth. +func (o *OAuthSession) MySavedComments(params ListingOptions) ([]*Comment, error) { + me, err := o.Me() + if err != nil { + return nil, err + } + return o.SavedComments(me.Name, params) +} diff --git a/oauth_session_test.go b/oauth_session_test.go new file mode 100644 index 0000000..83f5f37 --- /dev/null +++ b/oauth_session_test.go @@ -0,0 +1,84 @@ +// Copyright 2012 Jimmy Zelinskie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package geddit + +import ( + "errors" + "fmt" + "log" + "net/http" + "net/http/httptest" + "net/url" + "path" + "testing" +) + +type RewriteTransport struct { + Transport http.RoundTripper + URL *url.URL +} + +func (t RewriteTransport) RoundTrip(req *http.Request) (*http.Response, error) { + req.URL.Scheme = t.URL.Scheme + req.URL.Host = t.URL.Host + req.URL.Path = path.Join(t.URL.Path, req.URL.Path) + rt := t.Transport + if rt == nil { + rt = http.DefaultTransport + } + return rt.RoundTrip(req) +} + +func testTools(code int, body string) (*httptest.Server, *OAuthSession) { + // Dummy server to write JSON body provided + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(code) + w.Header().Set("Content-Type", "application/json") + fmt.Fprintln(w, body) + })) + + u, err := url.Parse(server.URL) + if err != nil { + log.Fatalf("Failed to parse local server URL: %v", err) + } + o := &OAuthSession{Client: http.DefaultClient, UserAgent: "Geddit Test"} + o.Client.Transport = RewriteTransport{URL: u} + + return server, o +} + +// Test defaults o fresh OAuthSession type. +func TestNewOAuthSession(t *testing.T) { + o, err := NewOAuthSession("user", "pw", "agent", "http://") + if err != nil { + t.Fatal(err) + } + + if o.Client != nil { + t.Fatal(errors.New("HTTP client created before auth token!")) + } +} + +func TestMe(t *testing.T) { + server, oauth := testTools(200, `{"has_mail": false, "name": "aggrolite", "is_friend": false, "created": 1278447313.0, "suspension_expiration_utc": null, "hide_from_robots": true, "is_suspended": false, "modhash": "XXX", "created_utc": 1278418513.0, "link_karma": 2327, "comment_karma": 1233, "over_18": true, "is_gold": false, "is_mod": true, "id": "45xiz", "gold_expiration": null, "inbox_count": 0, "has_verified_email": true, "gold_creddits": 0, "has_mod_mail": false}`) + defer server.Close() + + me, err := oauth.Me() + if err != nil { + t.Errorf("Me() Test failed: %v", err) + } + // Sanity check just a few fields? + if me.Name != "aggrolite" { + t.Fatalf("Me() returned unexpected name: %s", me.Name) + } + if me.ID != "45xiz" { + t.Fatalf("Me() returned unexpected ID: %s", me.ID) + } + if me.String() != "aggrolite (2327-1233)" { + t.Fatalf("Me.String() returns unexpected result: %s", me.String()) + } + fmt.Println(me) + +} diff --git a/redditor.go b/redditor.go index 0fcfc53..649b39e 100644 --- a/redditor.go +++ b/redditor.go @@ -9,15 +9,55 @@ import ( ) type Redditor struct { - ID string `json:"id"` - Name string `json:"name"` - LinkKarma int `json:"link_karma"` - CommentKarma int `json:"comment_karma"` - Created float32 `json:"created_utc"` - Gold bool `json:"is_gold"` - Mod bool `json:"is_mod"` - Mail *bool `json:"has_mail"` - ModMail *bool `json:"has_mod_mail"` + ID string `json:"id"` + Name string `json:"name"` + Created float32 `json:"created_utc"` + Gold bool `json:"is_gold"` + Mod bool `json:"is_mod"` + Mail *bool `json:"has_mail"` + ModMail *bool `json:"has_mod_mail"` + Karma +} + +type Preferences struct { + Research bool `json:"research"` + ShowStylesheets bool `json:"show_stylesheets"` + ShowLinkFlair bool `json:"show_link_flair"` + ShowTrending bool `json:"show_trending"` + PrivateFeeds bool `json:"private_feeds"` + IgnoreSuggestedSort bool `json:"ignore_suggested_sort"` + Media string `json:"media"` + ClickGadget bool `json:"clickgadget"` + LabelNSFW bool `json:"label_nsfw"` + Over18 bool `json:"over_18"` + EmailMessages bool `json:"email_messages"` + HighlightControversial bool `json:"highlight_controversial"` + ForceHTTPS bool `json:"force_https"` + Language string `json:"lang"` + HideFromRobots bool `json:"hide_from_robots"` + PublicVotes bool `json:"public_votes"` + ShowFlair bool `json:"show_flair"` + HideAds bool `json:"hide_ads"` + Beta bool `json:"beta"` + NewWindow bool `json:"newwindow"` + LegacySearch bool `json:"legacy_search"` +} + +type Friend struct { + Date float32 `json:"date"` + Name string `json:"name"` + ID string `json:"id"` +} + +type Karma struct { + CommentKarma int `json:"comment_karma"` + LinkKarma int `json:"link_karma"` +} + +type Trophy struct { + Name string `json:"name"` + Description string `json:"description"` + Icon string `json:"icon_70"` } // String returns the string representation of a reddit user. diff --git a/types.go b/types.go index 638861f..61076b8 100644 --- a/types.go +++ b/types.go @@ -88,3 +88,30 @@ type Deleter interface { type Replier interface { replyID() string } + +// OAuth constants + +const ( + OAuthScope_Identity string = "identity" + OAuthScope_Edit string = "edit" + OAuthScope_Flair string = "flair" + OAuthScope_History string = "history" + OAuthScope_Modconfig string = "modconfig" + OAuthScope_Modflair string = "modflair" + OAuthScope_Modlog string = "modlog" + OAuthScope_Modposts string = "modposts" + OAuthScope_Modwiki string = "modwiki" + OAuthScope_Mysubreddits string = "mysubreddits" + OAuthScope_Privatemessages string = "privatemessages" + OAuthScope_Read string = "read" + OAuthScope_Report string = "report" + OAuthScope_Save string = "save" + OAuthScope_Submit string = "submit" + OAuthScope_Subscribe string = "subscribe" + OAuthScope_Vote string = "vote" + OAuthScope_Wikiedit string = "wikiedit" + OAuthScope_Wikiread string = "wikiread" + + OAuthDuration_Temporary string = "temporary" + OAuthDuration_Permanent string = "permanent" +)