diff --git a/channels.go b/channels.go index 029337e4e..c99e66553 100644 --- a/channels.go +++ b/channels.go @@ -32,11 +32,7 @@ func (api *Client) channelRequest(ctx context.Context, path string, values url.V return nil, err } - if err := response.Err(); err != nil { - return nil, err - } - - return response, nil + return response, response.Err() } type channelsConfig struct { @@ -284,6 +280,7 @@ func (api *Client) GetChannelsContext(ctx context.Context, excludeArchived bool, "token": {api.token}, }, } + if excludeArchived { options = append(options, GetChannelsOptionExcludeArchived()) } diff --git a/chat.go b/chat.go index 25b5db36a..a480e5a7f 100644 --- a/chat.go +++ b/chat.go @@ -3,6 +3,7 @@ package slack import ( "context" "encoding/json" + "net/http" "net/url" "github.com/nlopes/slack/slackutilsx" @@ -25,7 +26,7 @@ const ( type chatResponseFull struct { Channel string `json:"channel"` - Timestamp string `json:"ts"` //Regualr message timestamp + Timestamp string `json:"ts"` //Regular message timestamp MessageTimeStamp string `json:"message_ts"` //Ephemeral message timestamp Text string `json:"text"` SlackResponse @@ -156,17 +157,18 @@ func (api *Client) SendMessage(channel string, options ...MsgOption) (string, st } // SendMessageContext more flexible method for configuring messages with a custom context. -func (api *Client) SendMessageContext(ctx context.Context, channelID string, options ...MsgOption) (channel string, timestamp string, text string, err error) { +func (api *Client) SendMessageContext(ctx context.Context, channelID string, options ...MsgOption) (_channel string, _timestamp string, _text string, err error) { var ( - config sendConfig + req *http.Request + parser func(*chatResponseFull) responseParser response chatResponseFull ) - if config, err = applyMsgOptions(api.token, channelID, api.endpoint, options...); err != nil { + if req, parser, err = buildSender(api.endpoint, options...).BuildRequest(api.token, channelID); err != nil { return "", "", "", err } - if err = postForm(ctx, api.httpclient, config.endpoint, config.values, &response, api); err != nil { + if err = doPost(ctx, api.httpclient, req, parser(&response), api); err != nil { return "", "", "", err } @@ -200,6 +202,13 @@ func applyMsgOptions(token, channel, apiurl string, options ...MsgOption) (sendC return config, nil } +func buildSender(apiurl string, options ...MsgOption) sendConfig { + return sendConfig{ + apiurl: apiurl, + options: options, + } +} + type sendMode string const ( @@ -207,16 +216,70 @@ const ( chatPostMessage sendMode = "chat.postMessage" chatDelete sendMode = "chat.delete" chatPostEphemeral sendMode = "chat.postEphemeral" + chatResponse sendMode = "chat.responseURL" chatMeMessage sendMode = "chat.meMessage" chatUnfurl sendMode = "chat.unfurl" ) type sendConfig struct { - apiurl string + apiurl string + options []MsgOption + mode sendMode + endpoint string + values url.Values + attachments []Attachment + responseType string +} + +func (t sendConfig) BuildRequest(token, channelID string) (req *http.Request, _ func(*chatResponseFull) responseParser, err error) { + if t, err = applyMsgOptions(token, channelID, t.apiurl, t.options...); err != nil { + return nil, nil, err + } + + switch t.mode { + case chatResponse: + return responseURLSender{ + endpoint: t.endpoint, + values: t.values, + attachments: t.attachments, + responseType: t.responseType, + }.BuildRequest() + default: + return formSender{endpoint: t.endpoint, values: t.values}.BuildRequest() + } +} + +type formSender struct { endpoint string values url.Values } +func (t formSender) BuildRequest() (*http.Request, func(*chatResponseFull) responseParser, error) { + req, err := formReq(t.endpoint, t.values) + return req, func(resp *chatResponseFull) responseParser { + return newJSONParser(resp) + }, err +} + +type responseURLSender struct { + endpoint string + values url.Values + attachments []Attachment + responseType string +} + +func (t responseURLSender) BuildRequest() (*http.Request, func(*chatResponseFull) responseParser, error) { + req, err := jsonReq(t.endpoint, Msg{ + Text: t.values.Get("text"), + Timestamp: t.values.Get("ts"), + Attachments: t.attachments, + ResponseType: t.responseType, + }) + return req, func(resp *chatResponseFull) responseParser { + return newContentTypeParser(resp) + }, err +} + // MsgOption option provided when sending a message. type MsgOption func(*sendConfig) error @@ -279,6 +342,17 @@ func MsgOptionUnfurl(timestamp string, unfurls map[string]Attachment) MsgOption } } +// MsgOptionResponseURL supplies a url to use as the endpoint. +func MsgOptionResponseURL(url string, rt string) MsgOption { + return func(config *sendConfig) error { + config.mode = chatResponse + config.endpoint = url + config.responseType = rt + config.values.Del("ts") + return nil + } +} + // MsgOptionAsUser whether or not to send the message as the user. func MsgOptionAsUser(b bool) MsgOption { return func(config *sendConfig) error { @@ -324,10 +398,17 @@ func MsgOptionAttachments(attachments ...Attachment) MsgOption { return nil } - attachments, err := json.Marshal(attachments) + config.attachments = attachments + + // FIXME: We are setting the attachments on the message twice: above for + // the json version, and below for the html version. The marshalled bytes + // we put into config.values below don't work directly in the Msg version. + + attachmentBytes, err := json.Marshal(attachments) if err == nil { - config.values.Set("attachments", string(attachments)) + config.values.Set("attachments", string(attachmentBytes)) } + return err } } diff --git a/conversation.go b/conversation.go index 47a54c54a..1e4a61f19 100644 --- a/conversation.go +++ b/conversation.go @@ -99,6 +99,7 @@ func (api *Client) GetUsersInConversationContext(ctx context.Context, params *Ge ResponseMetaData responseMetaData `json:"response_metadata"` SlackResponse }{} + err := api.postMethod(ctx, "conversations.members", values, &response) if err != nil { return nil, "", err @@ -160,6 +161,7 @@ func (api *Client) ArchiveConversationContext(ctx context.Context, channelID str "token": {api.token}, "channel": {channelID}, } + response := SlackResponse{} err := api.postMethod(ctx, "conversations.archive", values, &response) if err != nil { @@ -229,6 +231,7 @@ func (api *Client) SetPurposeOfConversationContext(ctx context.Context, channelI SlackResponse Channel *Channel `json:"channel"` }{} + err := api.postMethod(ctx, "conversations.setPurpose", values, &response) if err != nil { return nil, err @@ -253,6 +256,7 @@ func (api *Client) RenameConversationContext(ctx context.Context, channelID, cha SlackResponse Channel *Channel `json:"channel"` }{} + err := api.postMethod(ctx, "conversations.rename", values, &response) if err != nil { return nil, err @@ -277,6 +281,7 @@ func (api *Client) InviteUsersToConversationContext(ctx context.Context, channel SlackResponse Channel *Channel `json:"channel"` }{} + err := api.postMethod(ctx, "conversations.invite", values, &response) if err != nil { return nil, err @@ -297,6 +302,7 @@ func (api *Client) KickUserFromConversationContext(ctx context.Context, channelI "channel": {channelID}, "user": {user}, } + response := SlackResponse{} err := api.postMethod(ctx, "conversations.kick", values, &response) if err != nil { @@ -479,6 +485,7 @@ func (api *Client) GetConversationsContext(ctx context.Context, params *GetConve ResponseMetaData responseMetaData `json:"response_metadata"` SlackResponse }{} + err = api.postMethod(ctx, "conversations.list", values, &response) if err != nil { return nil, "", err @@ -516,6 +523,7 @@ func (api *Client) OpenConversationContext(ctx context.Context, params *OpenConv AlreadyOpen bool `json:"already_open"` SlackResponse }{} + err := api.postMethod(ctx, "conversations.open", values, &response) if err != nil { return nil, false, false, err @@ -540,6 +548,7 @@ func (api *Client) JoinConversationContext(ctx context.Context, channelID string } `json:"response_metadata"` SlackResponse }{} + err := api.postMethod(ctx, "conversations.join", values, &response) if err != nil { return nil, "", nil, err diff --git a/files.go b/files.go index 33fa3ba50..3a7363de2 100644 --- a/files.go +++ b/files.go @@ -321,6 +321,7 @@ func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParam } err = postWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.Filename, "file", values, params.Reader, response, api) } + if err != nil { return nil, err } diff --git a/files_test.go b/files_test.go index fa03512da..5361c06d3 100644 --- a/files_test.go +++ b/files_test.go @@ -177,7 +177,8 @@ func TestUploadFile(t *testing.T) { reader := bytes.NewBufferString("test reader") params = FileUploadParameters{ - Filename: "test.txt", Reader: reader, + Filename: "test.txt", + Reader: reader, Channels: []string{"CXXXXXXXX"}} if _, err := api.UploadFile(params); err != nil { t.Errorf("Unexpected error: %s", err) diff --git a/messages.go b/messages.go index 6cd3627af..37a26335d 100644 --- a/messages.go +++ b/messages.go @@ -98,6 +98,13 @@ type Msg struct { Blocks Blocks `json:"blocks,omitempty"` } +const ( + // ResponseTypeInChannel in channel response for slash commands. + ResponseTypeInChannel = "in_channel" + // ResponseTypeEphemeral ephemeral respone for slash commands. + ResponseTypeEphemeral = "ephemeral" +) + // Icon is used for bot messages type Icon struct { IconURL string `json:"icon_url,omitempty"` diff --git a/misc.go b/misc.go index 301be9795..0dcee950c 100644 --- a/misc.go +++ b/misc.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "io/ioutil" + "mime" "mime/multipart" "net/http" "net/http/httputil" @@ -80,8 +81,8 @@ func fileUploadReq(ctx context.Context, path string, values url.Values, r io.Rea if err != nil { return nil, err } - req = req.WithContext(ctx) + req = req.WithContext(ctx) req.URL.RawQuery = (values).Encode() return req, nil } @@ -117,6 +118,29 @@ func downloadFile(client httpClient, token string, downloadURL string, writer io return err } +func formReq(endpoint string, values url.Values) (req *http.Request, err error) { + if req, err = http.NewRequest("POST", endpoint, strings.NewReader(values.Encode())); err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + return req, nil +} + +func jsonReq(endpoint string, body interface{}) (req *http.Request, err error) { + buffer := bytes.NewBuffer([]byte{}) + if err = json.NewEncoder(buffer).Encode(body); err != nil { + return nil, err + } + + if req, err = http.NewRequest("POST", endpoint, buffer); err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json; charset=utf-8") + return req, nil +} + func parseResponseBody(body io.ReadCloser, intf interface{}, d debug) error { response, err := ioutil.ReadAll(body) if err != nil { @@ -130,7 +154,7 @@ func parseResponseBody(body io.ReadCloser, intf interface{}, d debug) error { return json.Unmarshal(response, intf) } -func postLocalWithMultipartResponse(ctx context.Context, client httpClient, path, fpath, fieldname string, values url.Values, intf interface{}, d debug) error { +func postLocalWithMultipartResponse(ctx context.Context, client httpClient, method, fpath, fieldname string, values url.Values, intf interface{}, d debug) error { fullpath, err := filepath.Abs(fpath) if err != nil { return err @@ -140,7 +164,8 @@ func postLocalWithMultipartResponse(ctx context.Context, client httpClient, path return err } defer file.Close() - return postWithMultipartResponse(ctx, client, path, filepath.Base(fpath), fieldname, values, file, intf, d) + + return postWithMultipartResponse(ctx, client, method, filepath.Base(fpath), fieldname, values, file, intf, d) } func postWithMultipartResponse(ctx context.Context, client httpClient, path, name, fieldname string, values url.Values, r io.Reader, intf interface{}, d debug) error { @@ -186,11 +211,11 @@ func postWithMultipartResponse(ctx context.Context, client httpClient, path, nam case err = <-errc: return err default: - return parseResponseBody(resp.Body, intf, d) + return newJSONParser(intf)(resp) } } -func doPost(ctx context.Context, client httpClient, req *http.Request, intf interface{}, d debug) error { +func doPost(ctx context.Context, client httpClient, req *http.Request, parser responseParser, d debug) error { req = req.WithContext(ctx) resp, err := client.Do(req) if err != nil { @@ -203,7 +228,7 @@ func doPost(ctx context.Context, client httpClient, req *http.Request, intf inte return err } - return parseResponseBody(resp.Body, intf, d) + return parser(resp) } // post JSON. @@ -215,7 +240,8 @@ func postJSON(ctx context.Context, client httpClient, endpoint, token string, js } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - return doPost(ctx, client, req, intf, d) + + return doPost(ctx, client, req, newJSONParser(intf), d) } // post a url encoded form. @@ -226,7 +252,7 @@ func postForm(ctx context.Context, client httpClient, endpoint string, values ur return err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - return doPost(ctx, client, req, intf, d) + return doPost(ctx, client, req, newJSONParser(intf), d) } func getResource(ctx context.Context, client httpClient, endpoint string, values url.Values, intf interface{}, d debug) error { @@ -237,7 +263,7 @@ func getResource(ctx context.Context, client httpClient, endpoint string, values req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.URL.RawQuery = values.Encode() - return doPost(ctx, client, req, intf, d) + return doPost(ctx, client, req, newJSONParser(intf), d) } func parseAdminResponse(ctx context.Context, client httpClient, method string, teamName string, values url.Values, intf interface{}, d debug) error { @@ -290,3 +316,45 @@ func checkStatusCode(resp *http.Response, d debug) error { return nil } + +type responseParser func(*http.Response) error + +func newJSONParser(dst interface{}) responseParser { + return func(resp *http.Response) error { + return json.NewDecoder(resp.Body).Decode(dst) + } +} + +func newTextParser(dst interface{}) responseParser { + return func(resp *http.Response) error { + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + if !bytes.Equal(b, []byte("ok")) { + return errors.New(string(b)) + } + + return nil + } +} + +func newContentTypeParser(dst interface{}) responseParser { + return func(req *http.Response) (err error) { + var ( + ctype string + ) + + if ctype, _, err = mime.ParseMediaType(req.Header.Get("Content-Type")); err != nil { + return err + } + + switch ctype { + case "application/json": + return newJSONParser(dst)(req) + default: + return newTextParser(dst)(req) + } + } +} diff --git a/misc_test.go b/misc_test.go index 441580e89..84121aee2 100644 --- a/misc_test.go +++ b/misc_test.go @@ -42,6 +42,7 @@ func TestParseResponse(t *testing.T) { values := url.Values{ "token": {validToken}, } + responsePartial := &SlackResponse{} err := postForm(context.Background(), http.DefaultClient, APIURL+"parseResponse", values, responsePartial, discard{}) if err != nil { @@ -54,6 +55,7 @@ func TestParseResponseNoToken(t *testing.T) { once.Do(startServer) APIURL := "http://" + serverAddr + "/" values := url.Values{} + responsePartial := &SlackResponse{} err := postForm(context.Background(), http.DefaultClient, APIURL+"parseResponse", values, responsePartial, discard{}) if err != nil { diff --git a/slack.go b/slack.go index c7b60110a..942305264 100644 --- a/slack.go +++ b/slack.go @@ -51,6 +51,8 @@ type authTestResponseFull struct { } // Client for the slack api. +type ParamOption func(*url.Values) + type Client struct { token string endpoint string diff --git a/users.go b/users.go index 84d8c5a19..4da8e4cec 100644 --- a/users.go +++ b/users.go @@ -420,13 +420,13 @@ func (api *Client) GetUserIdentity() (*UserIdentityResponse, error) { } // GetUserIdentityContext will retrieve user info available per identity scopes with a custom context -func (api *Client) GetUserIdentityContext(ctx context.Context) (*UserIdentityResponse, error) { +func (api *Client) GetUserIdentityContext(ctx context.Context) (response *UserIdentityResponse, err error) { values := url.Values{ "token": {api.token}, } - response := &UserIdentityResponse{} + response = &UserIdentityResponse{} - err := api.postMethod(ctx, "users.identity", values, response) + err = api.postMethod(ctx, "users.identity", values, response) if err != nil { return nil, err } @@ -444,7 +444,7 @@ func (api *Client) SetUserPhoto(image string, params UserSetPhotoParams) error { } // SetUserPhotoContext changes the currently authenticated user's profile image using a custom context -func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params UserSetPhotoParams) error { +func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params UserSetPhotoParams) (err error) { response := &SlackResponse{} values := url.Values{ "token": {api.token}, @@ -459,7 +459,7 @@ func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params values.Add("crop_w", strconv.Itoa(params.CropW)) } - err := postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"users.setPhoto", image, "image", values, response, api) + err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"users.setPhoto", image, "image", values, response, api) if err != nil { return err } @@ -473,13 +473,13 @@ func (api *Client) DeleteUserPhoto() error { } // DeleteUserPhotoContext deletes the current authenticated user's profile image with a custom context -func (api *Client) DeleteUserPhotoContext(ctx context.Context) error { +func (api *Client) DeleteUserPhotoContext(ctx context.Context) (err error) { response := &SlackResponse{} values := url.Values{ "token": {api.token}, } - err := api.postMethod(ctx, "users.deletePhoto", values, response) + err = api.postMethod(ctx, "users.deletePhoto", values, response) if err != nil { return err }