From f814c51b53d92263ff86fc28a704bcf18cf4a6d8 Mon Sep 17 00:00:00 2001 From: Vartan Benohanian Date: Wed, 16 Sep 2020 22:42:50 -0400 Subject: [PATCH] Add more methods to LiveThreadService - Create a live thread - Get a live thread's contributors - Accept an invite to contribute to a live thread - Leave a live thread Signed-off-by: Vartan Benohanian --- reddit/live-thread.go | 170 +++++++++++++++++- reddit/live-thread_test.go | 132 ++++++++++++++ .../live-thread/contributors-and-invited.json | 34 ++++ testdata/live-thread/contributors.json | 19 ++ 4 files changed, 354 insertions(+), 1 deletion(-) create mode 100644 testdata/live-thread/contributors-and-invited.json create mode 100644 testdata/live-thread/contributors.json diff --git a/reddit/live-thread.go b/reddit/live-thread.go index 50235c1..e3032bc 100644 --- a/reddit/live-thread.go +++ b/reddit/live-thread.go @@ -2,8 +2,13 @@ package reddit import ( "context" + "encoding/json" + "errors" "fmt" "net/http" + "net/url" + + "github.com/google/go-querystring/query" ) // LiveThreadService handles communication with the live thread @@ -28,16 +33,98 @@ type LiveThread struct { ViewerCount int `json:"viewer_count"` ViewerCountFuzzed bool `json:"viewer_count_fuzzed"` + // Empty when a list thread has ended. WebSocketURL string `json:"websocket_url,omitempty"` Announcement bool `json:"is_announcement"` NSFW bool `json:"nsfw"` } +// LiveThreadCreateRequest represents a request to create a live thread. +type LiveThreadCreateRequest struct { + // No longer than 120 characters. + Title string `url:"title"` + Description string `url:"description,omitempty"` + Resources string `url:"resources,omitempty"` + NSFW bool `url:"nsfw,omitempty"` +} + +// LiveThreadContributor is a user that can contribute to a live thread. +type LiveThreadContributor struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Permissions []string `json:"permissions,omitempty"` +} + +// LiveThreadContributors is a list of users that can contribute to a live thread. +type LiveThreadContributors struct { + Current []*LiveThreadContributor `json:"current_contributors"` + // This is only filled if you are a contributor in the live thread with the "manage" permission. + Invited []*LiveThreadContributor `json:"invited_contributors,omitempty"` +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (c *LiveThreadContributors) UnmarshalJSON(b []byte) error { + if len(b) == 0 { + return errors.New("no bytes to unmarshal") + } + + // neat trick taken from: + // https://www.calhoun.io/how-to-parse-json-that-varies-between-an-array-or-a-single-item-with-go + switch b[0] { + case '{': + return c.unmarshalSingle(b) + case '[': + return c.unmarshalMany(b) + } + + // This shouldn't really happen as the standard library seems to strip + // whitespace from the bytes being passed in, but just in case let's guess at + // multiple tags and fall back to a single one if that doesn't work. + err := c.unmarshalSingle(b) + if err != nil { + return c.unmarshalMany(b) + } + + return nil +} + +func (c *LiveThreadContributors) unmarshalSingle(b []byte) error { + root := new(struct { + Data struct { + Children []*LiveThreadContributor `json:"children"` + } `json:"data"` + }) + + err := json.Unmarshal(b, &root) + if err != nil { + return err + } + + c.Current = root.Data.Children + return nil +} + +func (c *LiveThreadContributors) unmarshalMany(b []byte) error { + var root [2]struct { + Data struct { + Children []*LiveThreadContributor `json:"children"` + } `json:"data"` + } + + err := json.Unmarshal(b, &root) + if err != nil { + return err + } + + c.Current = root[0].Data.Children + c.Invited = root[1].Data.Children + return nil +} + // Get information about a live thread. func (s *LiveThreadService) Get(ctx context.Context, id string) (*LiveThread, *Response, error) { path := fmt.Sprintf("live/%s/about", id) - req, err := s.client.NewRequest(http.MethodGet, path, nil) if err != nil { return nil, nil, err @@ -52,3 +139,84 @@ func (s *LiveThreadService) Get(ctx context.Context, id string) (*LiveThread, *R t, _ := root.LiveThread() return t, resp, nil } + +// Create a live thread and get its id. +func (s *LiveThreadService) Create(ctx context.Context, request *LiveThreadCreateRequest) (string, *Response, error) { + if request == nil { + return "", nil, errors.New("*LiveThreadCreateRequest: cannot be nil") + } + + form, err := query.Values(request) + if err != nil { + return "", nil, err + } + form.Set("api_type", "json") + + path := "api/live/create" + req, err := s.client.NewRequest(http.MethodPost, path, form) + if err != nil { + return "", nil, err + } + + root := new(struct { + JSON struct { + Data struct { + ID string `json:"id"` + } `json:"data"` + } `json:"json"` + }) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return "", resp, err + } + + return root.JSON.Data.ID, resp, nil +} + +// Contributors gets a list of users that are contributors to the live thread. +// If you are a contributor and you have the "manage" permission (to manage contributors), you +// also get a list of invited contributors that haven't yet accepted/refused their invitation. +func (s *LiveThreadService) Contributors(ctx context.Context, id string) (*LiveThreadContributors, *Response, error) { + path := fmt.Sprintf("live/%s/contributors", id) + req, err := s.client.NewRequest(http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(LiveThreadContributors) + resp, err := s.client.Do(ctx, req, &root) + if err != nil { + return nil, resp, err + } + + return root, resp, nil +} + +// Accept a pending invite to contribute to the live thread. +func (s *LiveThreadService) Accept(ctx context.Context, id string) (*Response, error) { + form := url.Values{} + form.Set("api_type", "json") + + path := fmt.Sprintf("api/live/%s/accept_contributor_invite", id) + req, err := s.client.NewRequest(http.MethodPost, path, form) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +// Leave the live thread by abdicating your status as contributor. +// todo: test as the author who leaves the thread. +func (s *LiveThreadService) Leave(ctx context.Context, id string) (*Response, error) { + form := url.Values{} + form.Set("api_type", "json") + + path := fmt.Sprintf("api/live/%s/leave_contributor", id) + req, err := s.client.NewRequest(http.MethodPost, path, form) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} diff --git a/reddit/live-thread_test.go b/reddit/live-thread_test.go index 658c43a..adffaa2 100644 --- a/reddit/live-thread_test.go +++ b/reddit/live-thread_test.go @@ -3,6 +3,7 @@ package reddit import ( "fmt" "net/http" + "net/url" "testing" "time" @@ -28,6 +29,24 @@ var expectedLiveThread = &LiveThread{ NSFW: false, } +var expectedLiveThreadContributors = &LiveThreadContributors{ + Current: []*LiveThreadContributor{ + {ID: "t2_test1", Name: "test1", Permissions: []string{"all"}}, + {ID: "t2_test2", Name: "test2", Permissions: []string{"all"}}, + }, + Invited: nil, +} + +var expectedLiveThreadContributorsAndInvited = &LiveThreadContributors{ + Current: []*LiveThreadContributor{ + {ID: "t2_test1", Name: "test1", Permissions: []string{"all"}}, + {ID: "t2_test2", Name: "test2", Permissions: []string{"all"}}, + }, + Invited: []*LiveThreadContributor{ + {ID: "t2_test3", Name: "test3", Permissions: []string{"manage", "discussions"}}, + }, +} + func TestLiveThreadService_Get(t *testing.T) { client, mux, teardown := setup() defer teardown() @@ -44,3 +63,116 @@ func TestLiveThreadService_Get(t *testing.T) { require.NoError(t, err) require.Equal(t, expectedLiveThread, liveThread) } + +func TestLiveThreadService_Create(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc("/api/live/create", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + + form := url.Values{} + form.Set("api_type", "json") + form.Set("title", "testtitle") + form.Set("description", "testdescription") + form.Set("resources", "testresources") + form.Set("nsfw", "true") + + err := r.ParseForm() + require.NoError(t, err) + require.Equal(t, form, r.PostForm) + + fmt.Fprint(w, `{ + "json": { + "data": { + "id": "id123" + }, + "errors": [] + } + }`) + }) + + _, _, err := client.LiveThread.Create(ctx, nil) + require.EqualError(t, err, "*LiveThreadCreateRequest: cannot be nil") + + id, _, err := client.LiveThread.Create(ctx, &LiveThreadCreateRequest{ + Title: "testtitle", + Description: "testdescription", + Resources: "testresources", + NSFW: true, + }) + require.NoError(t, err) + require.Equal(t, "id123", id) +} + +func TestLiveThreadService_Contributors(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + blob, err := readFileContents("../testdata/live-thread/contributors.json") + require.NoError(t, err) + + mux.HandleFunc("/live/id123/contributors", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method) + fmt.Fprint(w, blob) + }) + + contributors, _, err := client.LiveThread.Contributors(ctx, "id123") + require.NoError(t, err) + require.Equal(t, expectedLiveThreadContributors, contributors) +} + +func TestLiveThreadService_ContributorsAndInvited(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + blob, err := readFileContents("../testdata/live-thread/contributors-and-invited.json") + require.NoError(t, err) + + mux.HandleFunc("/live/id123/contributors", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method) + fmt.Fprint(w, blob) + }) + + contributors, _, err := client.LiveThread.Contributors(ctx, "id123") + require.NoError(t, err) + require.Equal(t, expectedLiveThreadContributorsAndInvited, contributors) +} + +func TestLiveThreadService_Accept(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc("/api/live/id123/accept_contributor_invite", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + + form := url.Values{} + form.Set("api_type", "json") + + err := r.ParseForm() + require.NoError(t, err) + require.Equal(t, form, r.PostForm) + }) + + _, err := client.LiveThread.Accept(ctx, "id123") + require.NoError(t, err) +} + +func TestLiveThreadService_Leave(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc("/api/live/id123/leave_contributor", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + + form := url.Values{} + form.Set("api_type", "json") + + err := r.ParseForm() + require.NoError(t, err) + require.Equal(t, form, r.PostForm) + }) + + _, err := client.LiveThread.Leave(ctx, "id123") + require.NoError(t, err) +} diff --git a/testdata/live-thread/contributors-and-invited.json b/testdata/live-thread/contributors-and-invited.json new file mode 100644 index 0000000..fb89b03 --- /dev/null +++ b/testdata/live-thread/contributors-and-invited.json @@ -0,0 +1,34 @@ +[ + { + "kind": "UserList", + "data": { + "children": [ + { + "rel_id": null, + "permissions": ["all"], + "id": "t2_test1", + "name": "test1" + }, + { + "rel_id": null, + "permissions": ["all"], + "id": "t2_test2", + "name": "test2" + } + ] + } + }, + { + "kind": "UserList", + "data": { + "children": [ + { + "rel_id": null, + "permissions": ["manage", "discussions"], + "id": "t2_test3", + "name": "test3" + } + ] + } + } +] diff --git a/testdata/live-thread/contributors.json b/testdata/live-thread/contributors.json new file mode 100644 index 0000000..d5ebdb3 --- /dev/null +++ b/testdata/live-thread/contributors.json @@ -0,0 +1,19 @@ +{ + "kind": "UserList", + "data": { + "children": [ + { + "rel_id": null, + "permissions": ["all"], + "id": "t2_test1", + "name": "test1" + }, + { + "rel_id": null, + "permissions": ["all"], + "id": "t2_test2", + "name": "test2" + } + ] + } +}