package slack import ( "context" "encoding/json" "fmt" "io" "net/url" "strconv" "strings" ) const ( // Add here the defaults in the siten DEFAULT_FILES_USER = "" DEFAULT_FILES_CHANNEL = "" DEFAULT_FILES_TS_FROM = 0 DEFAULT_FILES_TS_TO = -1 DEFAULT_FILES_TYPES = "all" DEFAULT_FILES_COUNT = 100 DEFAULT_FILES_PAGE = 1 DEFAULT_FILES_SHOW_HIDDEN = false ) // File contains all the information for a file type File struct { ID string `json:"id"` Created JSONTime `json:"created"` Timestamp JSONTime `json:"timestamp"` Name string `json:"name"` Title string `json:"title"` Mimetype string `json:"mimetype"` ImageExifRotation int `json:"image_exif_rotation"` Filetype string `json:"filetype"` PrettyType string `json:"pretty_type"` User string `json:"user"` Mode string `json:"mode"` Editable bool `json:"editable"` IsExternal bool `json:"is_external"` ExternalType string `json:"external_type"` Size int `json:"size"` URL string `json:"url"` // Deprecated - never set URLDownload string `json:"url_download"` // Deprecated - never set URLPrivate string `json:"url_private"` URLPrivateDownload string `json:"url_private_download"` OriginalH int `json:"original_h"` OriginalW int `json:"original_w"` Thumb64 string `json:"thumb_64"` Thumb80 string `json:"thumb_80"` Thumb160 string `json:"thumb_160"` Thumb360 string `json:"thumb_360"` Thumb360Gif string `json:"thumb_360_gif"` Thumb360W int `json:"thumb_360_w"` Thumb360H int `json:"thumb_360_h"` Thumb480 string `json:"thumb_480"` Thumb480W int `json:"thumb_480_w"` Thumb480H int `json:"thumb_480_h"` Thumb720 string `json:"thumb_720"` Thumb720W int `json:"thumb_720_w"` Thumb720H int `json:"thumb_720_h"` Thumb960 string `json:"thumb_960"` Thumb960W int `json:"thumb_960_w"` Thumb960H int `json:"thumb_960_h"` Thumb1024 string `json:"thumb_1024"` Thumb1024W int `json:"thumb_1024_w"` Thumb1024H int `json:"thumb_1024_h"` Permalink string `json:"permalink"` PermalinkPublic string `json:"permalink_public"` EditLink string `json:"edit_link"` Preview string `json:"preview"` PreviewHighlight string `json:"preview_highlight"` Lines int `json:"lines"` LinesMore int `json:"lines_more"` IsPublic bool `json:"is_public"` PublicURLShared bool `json:"public_url_shared"` Channels []string `json:"channels"` Groups []string `json:"groups"` IMs []string `json:"ims"` InitialComment Comment `json:"initial_comment"` CommentsCount int `json:"comments_count"` NumStars int `json:"num_stars"` IsStarred bool `json:"is_starred"` Shares Share `json:"shares"` } type Share struct { Public map[string][]ShareFileInfo `json:"public"` Private map[string][]ShareFileInfo `json:"private"` } type ShareFileInfo struct { ReplyUsers []string `json:"reply_users"` ReplyUsersCount int `json:"reply_users_count"` ReplyCount int `json:"reply_count"` Ts string `json:"ts"` ThreadTs string `json:"thread_ts"` LatestReply string `json:"latest_reply"` ChannelName string `json:"channel_name"` TeamID string `json:"team_id"` } // FileUploadParameters contains all the parameters necessary (including the optional ones) for an UploadFile() request. // // There are three ways to upload a file. You can either set Content if file is small, set Reader if file is large, // or provide a local file path in File to upload it from your filesystem. // // Note that when using the Reader option, you *must* specify the Filename, otherwise the Slack API isn't happy. type FileUploadParameters struct { File string Content string Reader io.Reader Filetype string Filename string Title string InitialComment string Channels []string ThreadTimestamp string } // GetFilesParameters contains all the parameters necessary (including the optional ones) for a GetFiles() request type GetFilesParameters struct { User string Channel string TeamID string TimestampFrom JSONTime TimestampTo JSONTime Types string Count int Page int ShowHidden bool } // ListFilesParameters contains all the parameters necessary (including the optional ones) for a ListFiles() request type ListFilesParameters struct { Limit int User string Channel string TeamID string Types string Cursor string } type UploadFileV2Parameters struct { File string FileSize int Content string Reader io.Reader Filename string Title string InitialComment string Channel string ThreadTimestamp string AltTxt string SnippetText string } type getUploadURLExternalParameters struct { altText string fileSize int fileName string snippetText string } type getUploadURLExternalResponse struct { UploadURL string `json:"upload_url"` FileID string `json:"file_id"` SlackResponse } type uploadToURLParameters struct { UploadURL string Reader io.Reader File string Content string Filename string } type FileSummary struct { ID string `json:"id"` Title string `json:"title"` } type completeUploadExternalParameters struct { title string channel string initialComment string threadTimestamp string } type completeUploadExternalResponse struct { SlackResponse Files []FileSummary `json:"files"` } type fileResponseFull struct { File `json:"file"` Paging `json:"paging"` Comments []Comment `json:"comments"` Files []File `json:"files"` Metadata ResponseMetadata `json:"response_metadata"` SlackResponse } // NewGetFilesParameters provides an instance of GetFilesParameters with all the sane default values set func NewGetFilesParameters() GetFilesParameters { return GetFilesParameters{ User: DEFAULT_FILES_USER, Channel: DEFAULT_FILES_CHANNEL, TimestampFrom: DEFAULT_FILES_TS_FROM, TimestampTo: DEFAULT_FILES_TS_TO, Types: DEFAULT_FILES_TYPES, Count: DEFAULT_FILES_COUNT, Page: DEFAULT_FILES_PAGE, ShowHidden: DEFAULT_FILES_SHOW_HIDDEN, } } func (api *Client) fileRequest(ctx context.Context, path string, values url.Values) (*fileResponseFull, error) { response := &fileResponseFull{} err := api.postMethod(ctx, path, values, response) if err != nil { return nil, err } return response, response.Err() } // GetFileInfo retrieves a file and related comments func (api *Client) GetFileInfo(fileID string, count, page int) (*File, []Comment, *Paging, error) { return api.GetFileInfoContext(context.Background(), fileID, count, page) } // GetFileInfoContext retrieves a file and related comments with a custom context func (api *Client) GetFileInfoContext(ctx context.Context, fileID string, count, page int) (*File, []Comment, *Paging, error) { values := url.Values{ "token": {api.token}, "file": {fileID}, "count": {strconv.Itoa(count)}, "page": {strconv.Itoa(page)}, } response, err := api.fileRequest(ctx, "files.info", values) if err != nil { return nil, nil, nil, err } return &response.File, response.Comments, &response.Paging, nil } // GetFile retrieves a given file from its private download URL func (api *Client) GetFile(downloadURL string, writer io.Writer) error { return api.GetFileContext(context.Background(), downloadURL, writer) } // GetFileContext retrieves a given file from its private download URL with a custom context // // For more details, see GetFile documentation. func (api *Client) GetFileContext(ctx context.Context, downloadURL string, writer io.Writer) error { return downloadFile(ctx, api.httpclient, api.token, downloadURL, writer, api) } // GetFiles retrieves all files according to the parameters given func (api *Client) GetFiles(params GetFilesParameters) ([]File, *Paging, error) { return api.GetFilesContext(context.Background(), params) } // GetFilesContext retrieves all files according to the parameters given with a custom context func (api *Client) GetFilesContext(ctx context.Context, params GetFilesParameters) ([]File, *Paging, error) { values := url.Values{ "token": {api.token}, } if params.User != DEFAULT_FILES_USER { values.Add("user", params.User) } if params.Channel != DEFAULT_FILES_CHANNEL { values.Add("channel", params.Channel) } if params.TeamID != "" { values.Add("team_id", params.TeamID) } if params.TimestampFrom != DEFAULT_FILES_TS_FROM { values.Add("ts_from", strconv.FormatInt(int64(params.TimestampFrom), 10)) } if params.TimestampTo != DEFAULT_FILES_TS_TO { values.Add("ts_to", strconv.FormatInt(int64(params.TimestampTo), 10)) } if params.Types != DEFAULT_FILES_TYPES { values.Add("types", params.Types) } if params.Count != DEFAULT_FILES_COUNT { values.Add("count", strconv.Itoa(params.Count)) } if params.Page != DEFAULT_FILES_PAGE { values.Add("page", strconv.Itoa(params.Page)) } if params.ShowHidden != DEFAULT_FILES_SHOW_HIDDEN { values.Add("show_files_hidden_by_limit", strconv.FormatBool(params.ShowHidden)) } response, err := api.fileRequest(ctx, "files.list", values) if err != nil { return nil, nil, err } return response.Files, &response.Paging, nil } // ListFiles retrieves all files according to the parameters given. Uses cursor based pagination. func (api *Client) ListFiles(params ListFilesParameters) ([]File, *ListFilesParameters, error) { return api.ListFilesContext(context.Background(), params) } // ListFilesContext retrieves all files according to the parameters given with a custom context. // // For more details, see ListFiles documentation. func (api *Client) ListFilesContext(ctx context.Context, params ListFilesParameters) ([]File, *ListFilesParameters, error) { values := url.Values{ "token": {api.token}, } if params.User != DEFAULT_FILES_USER { values.Add("user", params.User) } if params.Channel != DEFAULT_FILES_CHANNEL { values.Add("channel", params.Channel) } if params.TeamID != "" { values.Add("team_id", params.TeamID) } if params.Limit != DEFAULT_FILES_COUNT { values.Add("limit", strconv.Itoa(params.Limit)) } if params.Cursor != "" { values.Add("cursor", params.Cursor) } response, err := api.fileRequest(ctx, "files.list", values) if err != nil { return nil, nil, err } params.Cursor = response.Metadata.Cursor return response.Files, ¶ms, nil } // UploadFile uploads a file func (api *Client) UploadFile(params FileUploadParameters) (file *File, err error) { return api.UploadFileContext(context.Background(), params) } // UploadFileContext uploads a file and setting a custom context func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParameters) (file *File, err error) { // Test if user token is valid. This helps because client.Do doesn't like this for some reason. XXX: More // investigation needed, but for now this will do. _, err = api.AuthTestContext(ctx) if err != nil { return nil, err } response := &fileResponseFull{} values := url.Values{} if params.Filetype != "" { values.Add("filetype", params.Filetype) } if params.Filename != "" { values.Add("filename", params.Filename) } if params.Title != "" { values.Add("title", params.Title) } if params.InitialComment != "" { values.Add("initial_comment", params.InitialComment) } if params.ThreadTimestamp != "" { values.Add("thread_ts", params.ThreadTimestamp) } if len(params.Channels) != 0 { values.Add("channels", strings.Join(params.Channels, ",")) } if params.Content != "" { values.Add("content", params.Content) values.Add("token", api.token) err = api.postMethod(ctx, "files.upload", values, response) } else if params.File != "" { err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.File, "file", api.token, values, response, api) } else if params.Reader != nil { if params.Filename == "" { return nil, fmt.Errorf("files.upload: FileUploadParameters.Filename is mandatory when using FileUploadParameters.Reader") } err = postWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.Filename, "file", api.token, values, params.Reader, response, api) } if err != nil { return nil, err } return &response.File, response.Err() } // DeleteFileComment deletes a file's comment func (api *Client) DeleteFileComment(commentID, fileID string) error { return api.DeleteFileCommentContext(context.Background(), fileID, commentID) } // DeleteFileCommentContext deletes a file's comment with a custom context func (api *Client) DeleteFileCommentContext(ctx context.Context, fileID, commentID string) (err error) { if fileID == "" || commentID == "" { return ErrParametersMissing } values := url.Values{ "token": {api.token}, "file": {fileID}, "id": {commentID}, } _, err = api.fileRequest(ctx, "files.comments.delete", values) return err } // DeleteFile deletes a file func (api *Client) DeleteFile(fileID string) error { return api.DeleteFileContext(context.Background(), fileID) } // DeleteFileContext deletes a file with a custom context func (api *Client) DeleteFileContext(ctx context.Context, fileID string) (err error) { values := url.Values{ "token": {api.token}, "file": {fileID}, } _, err = api.fileRequest(ctx, "files.delete", values) return err } // RevokeFilePublicURL disables public/external sharing for a file func (api *Client) RevokeFilePublicURL(fileID string) (*File, error) { return api.RevokeFilePublicURLContext(context.Background(), fileID) } // RevokeFilePublicURLContext disables public/external sharing for a file with a custom context func (api *Client) RevokeFilePublicURLContext(ctx context.Context, fileID string) (*File, error) { values := url.Values{ "token": {api.token}, "file": {fileID}, } response, err := api.fileRequest(ctx, "files.revokePublicURL", values) if err != nil { return nil, err } return &response.File, nil } // ShareFilePublicURL enabled public/external sharing for a file func (api *Client) ShareFilePublicURL(fileID string) (*File, []Comment, *Paging, error) { return api.ShareFilePublicURLContext(context.Background(), fileID) } // ShareFilePublicURLContext enabled public/external sharing for a file with a custom context func (api *Client) ShareFilePublicURLContext(ctx context.Context, fileID string) (*File, []Comment, *Paging, error) { values := url.Values{ "token": {api.token}, "file": {fileID}, } response, err := api.fileRequest(ctx, "files.sharedPublicURL", values) if err != nil { return nil, nil, nil, err } return &response.File, response.Comments, &response.Paging, nil } // getUploadURLExternal gets a URL and fileID from slack which can later be used to upload a file func (api *Client) getUploadURLExternal(ctx context.Context, params getUploadURLExternalParameters) (*getUploadURLExternalResponse, error) { values := url.Values{ "token": {api.token}, "filename": {params.fileName}, "length": {strconv.Itoa(params.fileSize)}, } if params.altText != "" { values.Add("initial_comment", params.altText) } if params.snippetText != "" { values.Add("thread_ts", params.snippetText) } response := &getUploadURLExternalResponse{} err := api.postMethod(ctx, "files.getUploadURLExternal", values, response) if err != nil { return nil, err } return response, response.Err() } // uploadToURL uploads the file to the provided URL using post method func (api *Client) uploadToURL(ctx context.Context, params uploadToURLParameters) (err error) { values := url.Values{} if params.Content != "" { values.Add("content", params.Content) values.Add("token", api.token) err = postForm(ctx, api.httpclient, params.UploadURL, values, nil, api) } else if params.File != "" { err = postLocalWithMultipartResponse(ctx, api.httpclient, params.UploadURL, params.File, "file", api.token, values, nil, api) } else if params.Reader != nil { err = postWithMultipartResponse(ctx, api.httpclient, params.UploadURL, params.Filename, "file", api.token, values, params.Reader, nil, api) } return err } // completeUploadExternal once files are uploaded, this completes the upload and shares it to the specified channel func (api *Client) completeUploadExternal(ctx context.Context, fileID string, params completeUploadExternalParameters) (file *completeUploadExternalResponse, err error) { request := []FileSummary{{ID: fileID, Title: params.title}} requestBytes, err := json.Marshal(request) if err != nil { return nil, err } values := url.Values{ "token": {api.token}, "files": {string(requestBytes)}, "channel_id": {params.channel}, } if params.initialComment != "" { values.Add("initial_comment", params.initialComment) } if params.threadTimestamp != "" { values.Add("thread_ts", params.threadTimestamp) } response := &completeUploadExternalResponse{} err = api.postMethod(ctx, "files.completeUploadExternal", values, response) if err != nil { return nil, err } if response.Err() != nil { return nil, response.Err() } return response, nil } // UploadFileV2 uploads file to a given slack channel using 3 steps - // 1. Get an upload URL using files.getUploadURLExternal API // 2. Send the file as a post to the URL provided by slack // 3. Complete the upload and share it to the specified channel using files.completeUploadExternal func (api *Client) UploadFileV2(params UploadFileV2Parameters) (*FileSummary, error) { return api.UploadFileV2Context(context.Background(), params) } // UploadFileV2 uploads file to a given slack channel using 3 steps with a custom context - // 1. Get an upload URL using files.getUploadURLExternal API // 2. Send the file as a post to the URL provided by slack // 3. Complete the upload and share it to the specified channel using files.completeUploadExternal func (api *Client) UploadFileV2Context(ctx context.Context, params UploadFileV2Parameters) (file *FileSummary, err error) { if params.Filename == "" { return nil, fmt.Errorf("file.upload.v2: filename cannot be empty") } if params.FileSize == 0 { return nil, fmt.Errorf("file.upload.v2: file size cannot be 0") } if params.Channel == "" { return nil, fmt.Errorf("file.upload.v2: channel cannot be empty") } u, err := api.getUploadURLExternal(ctx, getUploadURLExternalParameters{ altText: params.AltTxt, fileName: params.Filename, fileSize: params.FileSize, snippetText: params.SnippetText, }) if err != nil { return nil, err } err = api.uploadToURL(ctx, uploadToURLParameters{ UploadURL: u.UploadURL, Reader: params.Reader, File: params.File, Content: params.Content, Filename: params.Filename, }) if err != nil { return nil, err } c, err := api.completeUploadExternal(ctx, u.FileID, completeUploadExternalParameters{ title: params.Title, channel: params.Channel, initialComment: params.InitialComment, threadTimestamp: params.ThreadTimestamp, }) if err != nil { return nil, err } if len(c.Files) != 1 { return nil, fmt.Errorf("file.upload.v2: something went wrong; received %d files instead of 1", len(c.Files)) } return &c.Files[0], nil }