diff --git a/message.go b/message.go index 4b027cf..e18efbd 100644 --- a/message.go +++ b/message.go @@ -2,10 +2,13 @@ package reddit import ( "context" + "encoding/json" "errors" "net/http" "net/url" "strings" + + "github.com/google/go-querystring/query" ) // MessageService handles communication with the message @@ -16,6 +19,106 @@ type MessageService struct { client *Client } +// Message is a message. +type Message struct { + ID string `json:"id"` + FullID string `json:"name"` + Created *Timestamp `json:"created_utc"` + + Subject string `json:"subject"` + Text string `json:"body"` + ParentID string `json:"parent_id"` + + Author string `json:"author"` + To string `json:"dest"` + + IsComment bool `json:"was_comment"` +} + +// Messages is a list of messages. +type Messages struct { + Messages []*Message `json:"messages"` + After string `json:"after"` + Before string `json:"before"` +} + +type rootInboxListing struct { + Kind string `json:"kind"` + Data inboxListing `json:"data"` +} + +type inboxListing struct { + Things inboxThings `json:"children"` + After string `json:"after"` + Before string `json:"before"` +} + +// The returned JSON for comments is a bit different. +// It looks for like the Message struct. +type inboxThings struct { + Comments []*Message + Messages []*Message +} + +// init initializes or clears the inbox. +func (t *inboxThings) init() { + t.Comments = make([]*Message, 0) + t.Messages = make([]*Message, 0) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (t *inboxThings) UnmarshalJSON(b []byte) error { + t.init() + + var things []thing + if err := json.Unmarshal(b, &things); err != nil { + return err + } + + for _, thing := range things { + switch thing.Kind { + case kindComment: + v := new(Message) + if err := json.Unmarshal(thing.Data, v); err == nil { + t.Comments = append(t.Comments, v) + } + case kindMessage: + v := new(Message) + if err := json.Unmarshal(thing.Data, v); err == nil { + t.Messages = append(t.Messages, v) + } + } + } + + return nil +} + +func (l *rootInboxListing) getComments() *Messages { + return &Messages{ + Messages: l.Data.Things.Comments, + After: l.Data.After, + Before: l.Data.Before, + } +} + +func (l *rootInboxListing) getMessages() *Messages { + return &Messages{ + Messages: l.Data.Things.Messages, + After: l.Data.After, + Before: l.Data.Before, + } +} + +// SendMessageRequest represents a request to send a message. +type SendMessageRequest struct { + // Username, or /r/name for that subreddit's moderators. + To string `url:"to"` + Subject string `url:"subject"` + Text string `url:"text"` + // Optional. If specified, the message will look like it came from the subreddit. + FromSubreddit string `url:"from_sr,omitempty"` +} + // ReadAll marks all messages/comments as read. It queues up the task on Reddit's end. // A successful response returns 202 to acknowledge acceptance of the request. // This endpoint is heavily rate limited. @@ -136,3 +239,72 @@ func (s *MessageService) Delete(ctx context.Context, id string) (*Response, erro return s.client.Do(ctx, req, nil) } + +// Send sends a message. +func (s *MessageService) Send(ctx context.Context, sendRequest *SendMessageRequest) (*Response, error) { + if sendRequest == nil { + return nil, errors.New("sendRequest: cannot be nil") + } + + path := "api/compose" + + form, err := query.Values(sendRequest) + if err != nil { + return nil, err + } + form.Set("api_type", "json") + + req, err := s.client.NewRequestWithForm(http.MethodPost, path, form) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +// Inbox returns comments and messages that appear in your inbox, respectively. +func (s *MessageService) Inbox(ctx context.Context, opts *ListOptions) (*Messages, *Messages, *Response, error) { + root, resp, err := s.inbox(ctx, "message/inbox", opts) + if err != nil { + return nil, nil, resp, err + } + return root.getComments(), root.getMessages(), resp, nil +} + +// InboxUnread returns unread comments and messages that appear in your inbox, respectively. +func (s *MessageService) InboxUnread(ctx context.Context, opts *ListOptions) (*Messages, *Messages, *Response, error) { + root, resp, err := s.inbox(ctx, "message/unread", opts) + if err != nil { + return nil, nil, resp, err + } + return root.getComments(), root.getMessages(), resp, nil +} + +// Sent returns messages that you've sent. +func (s *MessageService) Sent(ctx context.Context, opts *ListOptions) (*Messages, *Response, error) { + root, resp, err := s.inbox(ctx, "message/sent", opts) + if err != nil { + return nil, resp, err + } + return root.getMessages(), resp, nil +} + +func (s *MessageService) inbox(ctx context.Context, path string, opts *ListOptions) (*rootInboxListing, *Response, error) { + path, err := addOptions(path, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest(http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(rootInboxListing) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, nil, err + } + + return root, resp, nil +} diff --git a/message_test.go b/message_test.go index 93015c9..cb70a0b 100644 --- a/message_test.go +++ b/message_test.go @@ -1,13 +1,57 @@ package reddit import ( + "fmt" "net/http" "net/url" "testing" + "time" "github.com/stretchr/testify/require" ) +var expectedCommentMessages = &Messages{ + Messages: []*Message{ + { + ID: "g1xi2m9", + FullID: "t1_g1xi2m9", + Created: &Timestamp{time.Date(2020, 8, 18, 0, 24, 13, 0, time.UTC)}, + + Subject: "post reply", + Text: "u/testuser2 hello", + ParentID: "t3_hs03f3", + + Author: "testuser1", + To: "testuser2", + + IsComment: true, + }, + }, + After: "", + Before: "", +} + +var expectedMessages = &Messages{ + Messages: []*Message{ + { + ID: "qwki97", + FullID: "t4_qwki97", + Created: &Timestamp{time.Date(2020, 8, 18, 0, 16, 53, 0, time.UTC)}, + + Subject: "re: test", + Text: "test", + ParentID: "t4_qwki4m", + + Author: "testuser1", + To: "testuser2", + + IsComment: false, + }, + }, + After: "", + Before: "", +} + func TestMessageService_ReadAll(t *testing.T) { setup() defer teardown() @@ -147,3 +191,87 @@ func TestMessageService_Delete(t *testing.T) { _, err := client.Message.Delete(ctx, "test") require.NoError(t, err) } + +func TestMessageService_Send(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/api/compose", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + + form := url.Values{} + form.Set("api_type", "json") + form.Set("to", "test") + form.Set("subject", "test subject") + form.Set("text", "test text") + form.Set("from_sr", "hello world") + + err := r.ParseForm() + require.NoError(t, err) + require.Equal(t, form, r.Form) + }) + + _, err := client.Message.Send(ctx, nil) + require.EqualError(t, err, "sendRequest: cannot be nil") + + _, err = client.Message.Send(ctx, &SendMessageRequest{ + To: "test", + Subject: "test subject", + Text: "test text", + FromSubreddit: "hello world", + }) + require.NoError(t, err) +} + +func TestMessageService_Inbox(t *testing.T) { + setup() + defer teardown() + + blob, err := readFileContents("testdata/message/inbox.json") + require.NoError(t, err) + + mux.HandleFunc("/message/inbox", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method) + fmt.Fprint(w, blob) + }) + + comments, messages, _, err := client.Message.Inbox(ctx, nil) + require.NoError(t, err) + require.Equal(t, expectedCommentMessages, comments) + require.Equal(t, expectedMessages, messages) +} + +func TestMessageService_InboxUnread(t *testing.T) { + setup() + defer teardown() + + blob, err := readFileContents("testdata/message/inbox.json") + require.NoError(t, err) + + mux.HandleFunc("/message/unread", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method) + fmt.Fprint(w, blob) + }) + + comments, messages, _, err := client.Message.InboxUnread(ctx, nil) + require.NoError(t, err) + require.Equal(t, expectedCommentMessages, comments) + require.Equal(t, expectedMessages, messages) +} + +func TestMessageService_Sent(t *testing.T) { + setup() + defer teardown() + + blob, err := readFileContents("testdata/message/inbox.json") + require.NoError(t, err) + + mux.HandleFunc("/message/sent", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method) + fmt.Fprint(w, blob) + }) + + messages, _, err := client.Message.Sent(ctx, nil) + require.NoError(t, err) + require.Equal(t, expectedMessages, messages) +} diff --git a/testdata/message/inbox.json b/testdata/message/inbox.json new file mode 100644 index 0000000..776fdd2 --- /dev/null +++ b/testdata/message/inbox.json @@ -0,0 +1,70 @@ +{ + "kind": "Listing", + "data": { + "modhash": null, + "dist": 2, + "children": [ + { + "kind": "t1", + "data": { + "first_message": null, + "first_message_name": null, + "subreddit": "helloworldtestt", + "likes": null, + "replies": "", + "id": "g1xi2m9", + "subject": "post reply", + "associated_awarding_id": null, + "score": 1, + "author": "testuser1", + "num_comments": 17, + "parent_id": "t3_hs03f3", + "subreddit_name_prefixed": "r/helloworldtestt", + "new": false, + "type": "post_reply", + "body": "u/testuser2 hello", + "link_title": "post 1", + "dest": "testuser2", + "was_comment": true, + "body_html": "<!-- SC_OFF --><div class=\"md\"><p><a href=\"/u/testuser2\">u/testuser2</a> hello</p>\n</div><!-- SC_ON -->", + "name": "t1_g1xi2m9", + "created": 1597739053.0, + "created_utc": 1597710253.0, + "context": "/r/helloworldtestt/comments/hs03f3/post_1/g1xi2m9/?context=3", + "distinguished": null + } + }, + { + "kind": "t4", + "data": { + "first_message": 1626823824, + "first_message_name": "t4_qwkhao", + "subreddit": null, + "likes": null, + "replies": "", + "id": "qwki97", + "subject": "re: test", + "associated_awarding_id": null, + "score": 0, + "author": "testuser1", + "num_comments": null, + "parent_id": "t4_qwki4m", + "subreddit_name_prefixed": null, + "new": false, + "type": "unknown", + "body": "test", + "dest": "testuser2", + "body_html": "<!-- SC_OFF --><div class=\"md\"><p>test</p>\n</div><!-- SC_ON -->", + "was_comment": false, + "name": "t4_qwki97", + "created": 1597738613.0, + "created_utc": 1597709813.0, + "context": "", + "distinguished": null + } + } + ], + "after": "", + "before": null + } +} diff --git a/things.go b/things.go index 5424199..efe09f7 100644 --- a/things.go +++ b/things.go @@ -106,7 +106,6 @@ func (t *things) UnmarshalJSON(b []byte) error { if err := json.Unmarshal(thing.Data, v); err == nil { t.Posts = append(t.Posts, v) } - case kindMessage: case kindSubreddit: v := new(Subreddit) if err := json.Unmarshal(thing.Data, v); err == nil { @@ -389,7 +388,7 @@ type Posts struct { Before string `json:"before"` } -// ModActions is a list of moderator action. +// ModActions is a list of moderator actions. type ModActions struct { ModActions []*ModAction `json:"moderator_actions"` After string `json:"after"`