diff --git a/account.go b/account.go index 01f7dd8..7a39b4d 100644 --- a/account.go +++ b/account.go @@ -2,9 +2,8 @@ package geddit import ( "context" - "encoding/json" - "errors" "net/http" + "net/url" ) // AccountService handles communication with the account @@ -28,9 +27,10 @@ type SubredditKarma struct { // Settings are the user's account settings. // Some of the fields' descriptions are taken from: // https://praw.readthedocs.io/en/latest/code_overview/other/preferences.html#praw.models.Preferences.update -// todo: these should probably be pointers with omitempty type Settings struct { - // Control whose private messages you see: "everyone" or "whitelisted". + // Control whose private messages you see. + // - "everyone": everyone except blocked users + // - "whitelisted": only trusted users AcceptPrivateMessages *string `json:"accept_pms,omitempty"` // Allow Reddit to use your activity on Reddit to show you more relevant advertisements. ActivityRelevantAds *bool `json:"activity_relevant_ads,omitempty"` @@ -209,6 +209,13 @@ type Settings struct { EnableVideoAutoplay *bool `json:"video_autoplay,omitempty"` } +type rootRelationshipList struct { + Kind string `json:"kind,omitempty"` + Data struct { + Relationships []Relationship `json:"children"` + } `json:"data"` +} + // Info returns some general information about your account. func (s *AccountService) Info(ctx context.Context) (*User, *Response, error) { path := "api/v1/me" @@ -296,6 +303,7 @@ func (s *AccountService) Trophies(ctx context.Context) ([]Trophy, *Response, err return nil, resp, err } + // todo: use Things struct var trophies []Trophy for _, trophy := range root.Data.Trophies { trophies = append(trophies, trophy.Data) @@ -304,52 +312,6 @@ func (s *AccountService) Trophies(ctx context.Context) ([]Trophy, *Response, err return trophies, resp, nil } -type rootFriendList struct { - Friends []Relationship -} - -// UnmarshalJSON implements the json.Unmarshaler interface. -func (l *rootFriendList) UnmarshalJSON(b []byte) error { - var resBody []interface{} - err := json.Unmarshal(b, &resBody) - if err != nil { - return err - } - - if len(resBody) == 0 { - return errors.New("unexpected data length received") - } - - data, ok := resBody[0].(map[string]interface{}) - if !ok { - return errors.New("unexpected data type received") - } - - dataMap, ok := data["data"].(map[string]interface{}) - if !ok { - return errors.New("data does not contain expected field") - } - - children, ok := dataMap["children"].([]interface{}) - if !ok { - return errors.New("data does not contain expected field") - } - - byteValue, err := json.Marshal(children) - if err != nil { - return nil - } - - var friends []Relationship - err = json.Unmarshal(byteValue, &friends) - if err != nil { - return err - } - - l.Friends = friends - return nil -} - // Friends returns a list of your friends. func (s *AccountService) Friends(ctx context.Context) ([]Relationship, *Response, error) { path := "prefs/friends" @@ -359,20 +321,13 @@ func (s *AccountService) Friends(ctx context.Context) ([]Relationship, *Response return nil, nil, err } - root := new(rootFriendList) - resp, err := s.client.Do(ctx, req, root) + root := make([]rootRelationshipList, 2) + resp, err := s.client.Do(ctx, req, &root) if err != nil { return nil, resp, err } - return root.Friends, resp, nil -} - -type rootBlockedListing struct { - Kind string `json:"kind,omitempty"` - Data struct { - Blocked []Relationship `json:"children"` - } `json:"data"` + return root[0].Data.Relationships, resp, nil } // Blocked returns a list of your blocked users. @@ -384,11 +339,85 @@ func (s *AccountService) Blocked(ctx context.Context) ([]Relationship, *Response return nil, nil, err } - root := new(rootBlockedListing) + root := new(rootRelationshipList) resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } - return root.Data.Blocked, resp, nil + return root.Data.Relationships, resp, nil +} + +// Messaging returns blocked users and trusted users, respectively. +func (s *AccountService) Messaging(ctx context.Context) ([]Relationship, []Relationship, *Response, error) { + path := "prefs/messaging" + + req, err := s.client.NewRequest(http.MethodGet, path, nil) + if err != nil { + return nil, nil, nil, err + } + + root := make([]rootRelationshipList, 2) + resp, err := s.client.Do(ctx, req, &root) + if err != nil { + return nil, nil, resp, err + } + + blocked := root[0].Data.Relationships + trusted := root[1].Data.Relationships + + return blocked, trusted, resp, nil +} + +// Trusted returns a list of your trusted users. +func (s *AccountService) Trusted(ctx context.Context) ([]Relationship, *Response, error) { + path := "prefs/trusted" + + req, err := s.client.NewRequest(http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(rootRelationshipList) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Data.Relationships, resp, nil +} + +// AddTrusted adds a user to your trusted users. +// This is not visible in the Reddit API docs. +func (s *AccountService) AddTrusted(ctx context.Context, username string) (*Response, error) { + path := "api/add_whitelisted" + + form := url.Values{} + form.Set("api_type", "json") + form.Set("name", username) + // todo: you can also do this with the user id. form.Set("id", id). should we? or is this enough? + + req, err := s.client.NewPostForm(path, form) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +// RemoveTrusted removes a user from your trusted users. +// This is not visible in the Reddit API docs. +func (s *AccountService) RemoveTrusted(ctx context.Context, username string) (*Response, error) { + path := "api/remove_whitelisted" + + form := url.Values{} + form.Set("name", username) + // todo: you can also do this with the user id. form.Set("id", id). should we? or is this enough? + + req, err := s.client.NewPostForm(path, form) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) } diff --git a/account_test.go b/account_test.go index 3fc8679..ea8470a 100644 --- a/account_test.go +++ b/account_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "net/url" "testing" "time" @@ -112,6 +113,15 @@ var expectedRelationships = []Relationship{ }, } +var expectedRelationships2 = []Relationship{ + { + ID: "r9_1re60i", + User: "test3", + UserID: "t2_test3", + Created: &Timestamp{time.Date(2020, 3, 6, 2, 27, 0, 0, time.UTC)}, + }, +} + func TestAccountService_Info(t *testing.T) { setup() defer teardown() @@ -230,3 +240,75 @@ func TestAccountService_Blocked(t *testing.T) { assert.NoError(t, err) assert.Equal(t, expectedRelationships, relationships) } + +func TestAccountService_Messaging(t *testing.T) { + setup() + defer teardown() + + blob := readFileContents(t, "testdata/account/messaging.json") + + mux.HandleFunc("/prefs/messaging", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + fmt.Fprint(w, blob) + }) + + blocked, trusted, _, err := client.Account.Messaging(ctx) + assert.NoError(t, err) + assert.Equal(t, expectedRelationships, blocked) + assert.Equal(t, expectedRelationships2, trusted) +} + +func TestAccountService_Trusted(t *testing.T) { + setup() + defer teardown() + + blob := readFileContents(t, "testdata/account/trusted.json") + + mux.HandleFunc("/prefs/trusted", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + fmt.Fprint(w, blob) + }) + + relationships, _, err := client.Account.Trusted(ctx) + assert.NoError(t, err) + assert.Equal(t, expectedRelationships, relationships) +} + +func TestAccountService_AddTrusted(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/api/add_whitelisted", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method) + + form := url.Values{} + form.Set("api_type", "json") + form.Set("name", "test123") + + err := r.ParseForm() + assert.NoError(t, err) + assert.Equal(t, form, r.Form) + }) + + _, err := client.Account.AddTrusted(ctx, "test123") + assert.NoError(t, err) +} + +func TestAccountService_RemoveTrusted(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/api/remove_whitelisted", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method) + + form := url.Values{} + form.Set("name", "test123") + + err := r.ParseForm() + assert.NoError(t, err) + assert.Equal(t, form, r.Form) + }) + + _, err := client.Account.RemoveTrusted(ctx, "test123") + assert.NoError(t, err) +} diff --git a/errors.go b/errors.go index c796abb..178487d 100644 --- a/errors.go +++ b/errors.go @@ -14,7 +14,7 @@ type RedditError struct { } func (e *RedditError) Error() string { - return fmt.Sprintf("%s: %s because of field %q", e.Label, e.Reason, e.Field) + return fmt.Sprintf("field %q caused %s: %s", e.Field, e.Label, e.Reason) } // UnmarshalJSON implements the json.Unmarshaler interface. diff --git a/testdata/account/messaging.json b/testdata/account/messaging.json new file mode 100644 index 0000000..0087c80 --- /dev/null +++ b/testdata/account/messaging.json @@ -0,0 +1,34 @@ +[ + { + "kind": "UserList", + "data": { + "children": [ + { + "date": 1593362635, + "rel_id": "r9_1r4879", + "name": "test1", + "id": "t2_test1" + }, + { + "date": 1593362642, + "rel_id": "r9_1re930", + "name": "test2", + "id": "t2_test2" + } + ] + } + }, + { + "kind": "UserList", + "data": { + "children": [ + { + "date": 1583461620, + "rel_id": "r9_1re60i", + "name": "test3", + "id": "t2_test3" + } + ] + } + } +] diff --git a/testdata/account/trusted.json b/testdata/account/trusted.json new file mode 100644 index 0000000..7177b87 --- /dev/null +++ b/testdata/account/trusted.json @@ -0,0 +1,19 @@ +{ + "kind": "UserList", + "data": { + "children": [ + { + "date": 1593362635, + "rel_id": "r9_1r4879", + "name": "test1", + "id": "t2_test1" + }, + { + "date": 1593362642, + "rel_id": "r9_1re930", + "name": "test2", + "id": "t2_test2" + } + ] + } +}