1package slack 2 3import ( 4 "context" 5 "fmt" 6 "io" 7 "net/url" 8 "strconv" 9 "strings" 10) 11 12const ( 13 // Add here the defaults in the siten 14 DEFAULT_FILES_USER = "" 15 DEFAULT_FILES_CHANNEL = "" 16 DEFAULT_FILES_TS_FROM = 0 17 DEFAULT_FILES_TS_TO = -1 18 DEFAULT_FILES_TYPES = "all" 19 DEFAULT_FILES_COUNT = 100 20 DEFAULT_FILES_PAGE = 1 21 DEFAULT_FILES_SHOW_HIDDEN = false 22) 23 24// File contains all the information for a file 25type File struct { 26 ID string `json:"id"` 27 Created JSONTime `json:"created"` 28 Timestamp JSONTime `json:"timestamp"` 29 30 Name string `json:"name"` 31 Title string `json:"title"` 32 Mimetype string `json:"mimetype"` 33 ImageExifRotation int `json:"image_exif_rotation"` 34 Filetype string `json:"filetype"` 35 PrettyType string `json:"pretty_type"` 36 User string `json:"user"` 37 38 Mode string `json:"mode"` 39 Editable bool `json:"editable"` 40 IsExternal bool `json:"is_external"` 41 ExternalType string `json:"external_type"` 42 43 Size int `json:"size"` 44 45 URL string `json:"url"` // Deprecated - never set 46 URLDownload string `json:"url_download"` // Deprecated - never set 47 URLPrivate string `json:"url_private"` 48 URLPrivateDownload string `json:"url_private_download"` 49 50 OriginalH int `json:"original_h"` 51 OriginalW int `json:"original_w"` 52 Thumb64 string `json:"thumb_64"` 53 Thumb80 string `json:"thumb_80"` 54 Thumb160 string `json:"thumb_160"` 55 Thumb360 string `json:"thumb_360"` 56 Thumb360Gif string `json:"thumb_360_gif"` 57 Thumb360W int `json:"thumb_360_w"` 58 Thumb360H int `json:"thumb_360_h"` 59 Thumb480 string `json:"thumb_480"` 60 Thumb480W int `json:"thumb_480_w"` 61 Thumb480H int `json:"thumb_480_h"` 62 Thumb720 string `json:"thumb_720"` 63 Thumb720W int `json:"thumb_720_w"` 64 Thumb720H int `json:"thumb_720_h"` 65 Thumb960 string `json:"thumb_960"` 66 Thumb960W int `json:"thumb_960_w"` 67 Thumb960H int `json:"thumb_960_h"` 68 Thumb1024 string `json:"thumb_1024"` 69 Thumb1024W int `json:"thumb_1024_w"` 70 Thumb1024H int `json:"thumb_1024_h"` 71 72 Permalink string `json:"permalink"` 73 PermalinkPublic string `json:"permalink_public"` 74 75 EditLink string `json:"edit_link"` 76 Preview string `json:"preview"` 77 PreviewHighlight string `json:"preview_highlight"` 78 Lines int `json:"lines"` 79 LinesMore int `json:"lines_more"` 80 81 IsPublic bool `json:"is_public"` 82 PublicURLShared bool `json:"public_url_shared"` 83 Channels []string `json:"channels"` 84 Groups []string `json:"groups"` 85 IMs []string `json:"ims"` 86 InitialComment Comment `json:"initial_comment"` 87 CommentsCount int `json:"comments_count"` 88 NumStars int `json:"num_stars"` 89 IsStarred bool `json:"is_starred"` 90 Shares Share `json:"shares"` 91} 92 93type Share struct { 94 Public map[string][]ShareFileInfo `json:"public"` 95 Private map[string][]ShareFileInfo `json:"private"` 96} 97 98type ShareFileInfo struct { 99 ReplyUsers []string `json:"reply_users"` 100 ReplyUsersCount int `json:"reply_users_count"` 101 ReplyCount int `json:"reply_count"` 102 Ts string `json:"ts"` 103 ThreadTs string `json:"thread_ts"` 104 LatestReply string `json:"latest_reply"` 105 ChannelName string `json:"channel_name"` 106 TeamID string `json:"team_id"` 107} 108 109// FileUploadParameters contains all the parameters necessary (including the optional ones) for an UploadFile() request. 110// 111// There are three ways to upload a file. You can either set Content if file is small, set Reader if file is large, 112// or provide a local file path in File to upload it from your filesystem. 113// 114// Note that when using the Reader option, you *must* specify the Filename, otherwise the Slack API isn't happy. 115type FileUploadParameters struct { 116 File string 117 Content string 118 Reader io.Reader 119 Filetype string 120 Filename string 121 Title string 122 InitialComment string 123 Channels []string 124 ThreadTimestamp string 125} 126 127// GetFilesParameters contains all the parameters necessary (including the optional ones) for a GetFiles() request 128type GetFilesParameters struct { 129 User string 130 Channel string 131 TimestampFrom JSONTime 132 TimestampTo JSONTime 133 Types string 134 Count int 135 Page int 136 ShowHidden bool 137} 138 139// ListFilesParameters contains all the parameters necessary (including the optional ones) for a ListFiles() request 140type ListFilesParameters struct { 141 Limit int 142 User string 143 Channel string 144 Types string 145 Cursor string 146} 147 148type fileResponseFull struct { 149 File `json:"file"` 150 Paging `json:"paging"` 151 Comments []Comment `json:"comments"` 152 Files []File `json:"files"` 153 Metadata ResponseMetadata `json:"response_metadata"` 154 155 SlackResponse 156} 157 158// NewGetFilesParameters provides an instance of GetFilesParameters with all the sane default values set 159func NewGetFilesParameters() GetFilesParameters { 160 return GetFilesParameters{ 161 User: DEFAULT_FILES_USER, 162 Channel: DEFAULT_FILES_CHANNEL, 163 TimestampFrom: DEFAULT_FILES_TS_FROM, 164 TimestampTo: DEFAULT_FILES_TS_TO, 165 Types: DEFAULT_FILES_TYPES, 166 Count: DEFAULT_FILES_COUNT, 167 Page: DEFAULT_FILES_PAGE, 168 ShowHidden: DEFAULT_FILES_SHOW_HIDDEN, 169 } 170} 171 172func (api *Client) fileRequest(ctx context.Context, path string, values url.Values) (*fileResponseFull, error) { 173 response := &fileResponseFull{} 174 err := api.postMethod(ctx, path, values, response) 175 if err != nil { 176 return nil, err 177 } 178 179 return response, response.Err() 180} 181 182// GetFileInfo retrieves a file and related comments 183func (api *Client) GetFileInfo(fileID string, count, page int) (*File, []Comment, *Paging, error) { 184 return api.GetFileInfoContext(context.Background(), fileID, count, page) 185} 186 187// GetFileInfoContext retrieves a file and related comments with a custom context 188func (api *Client) GetFileInfoContext(ctx context.Context, fileID string, count, page int) (*File, []Comment, *Paging, error) { 189 values := url.Values{ 190 "token": {api.token}, 191 "file": {fileID}, 192 "count": {strconv.Itoa(count)}, 193 "page": {strconv.Itoa(page)}, 194 } 195 196 response, err := api.fileRequest(ctx, "files.info", values) 197 if err != nil { 198 return nil, nil, nil, err 199 } 200 return &response.File, response.Comments, &response.Paging, nil 201} 202 203// GetFile retreives a given file from its private download URL 204func (api *Client) GetFile(downloadURL string, writer io.Writer) error { 205 return downloadFile(api.httpclient, api.token, downloadURL, writer, api) 206} 207 208// GetFiles retrieves all files according to the parameters given 209func (api *Client) GetFiles(params GetFilesParameters) ([]File, *Paging, error) { 210 return api.GetFilesContext(context.Background(), params) 211} 212 213// ListFiles retrieves all files according to the parameters given. Uses cursor based pagination. 214func (api *Client) ListFiles(params ListFilesParameters) ([]File, *ListFilesParameters, error) { 215 return api.ListFilesContext(context.Background(), params) 216} 217 218// ListFilesContext retrieves all files according to the parameters given with a custom context. Uses cursor based pagination. 219func (api *Client) ListFilesContext(ctx context.Context, params ListFilesParameters) ([]File, *ListFilesParameters, error) { 220 values := url.Values{ 221 "token": {api.token}, 222 } 223 224 if params.User != DEFAULT_FILES_USER { 225 values.Add("user", params.User) 226 } 227 if params.Channel != DEFAULT_FILES_CHANNEL { 228 values.Add("channel", params.Channel) 229 } 230 if params.Limit != DEFAULT_FILES_COUNT { 231 values.Add("limit", strconv.Itoa(params.Limit)) 232 } 233 if params.Cursor != "" { 234 values.Add("cursor", params.Cursor) 235 } 236 237 response, err := api.fileRequest(ctx, "files.list", values) 238 if err != nil { 239 return nil, nil, err 240 } 241 242 params.Cursor = response.Metadata.Cursor 243 244 return response.Files, ¶ms, nil 245} 246 247// GetFilesContext retrieves all files according to the parameters given with a custom context 248func (api *Client) GetFilesContext(ctx context.Context, params GetFilesParameters) ([]File, *Paging, error) { 249 values := url.Values{ 250 "token": {api.token}, 251 } 252 if params.User != DEFAULT_FILES_USER { 253 values.Add("user", params.User) 254 } 255 if params.Channel != DEFAULT_FILES_CHANNEL { 256 values.Add("channel", params.Channel) 257 } 258 if params.TimestampFrom != DEFAULT_FILES_TS_FROM { 259 values.Add("ts_from", strconv.FormatInt(int64(params.TimestampFrom), 10)) 260 } 261 if params.TimestampTo != DEFAULT_FILES_TS_TO { 262 values.Add("ts_to", strconv.FormatInt(int64(params.TimestampTo), 10)) 263 } 264 if params.Types != DEFAULT_FILES_TYPES { 265 values.Add("types", params.Types) 266 } 267 if params.Count != DEFAULT_FILES_COUNT { 268 values.Add("count", strconv.Itoa(params.Count)) 269 } 270 if params.Page != DEFAULT_FILES_PAGE { 271 values.Add("page", strconv.Itoa(params.Page)) 272 } 273 if params.ShowHidden != DEFAULT_FILES_SHOW_HIDDEN { 274 values.Add("show_files_hidden_by_limit", strconv.FormatBool(params.ShowHidden)) 275 } 276 277 response, err := api.fileRequest(ctx, "files.list", values) 278 if err != nil { 279 return nil, nil, err 280 } 281 return response.Files, &response.Paging, nil 282} 283 284// UploadFile uploads a file 285func (api *Client) UploadFile(params FileUploadParameters) (file *File, err error) { 286 return api.UploadFileContext(context.Background(), params) 287} 288 289// UploadFileContext uploads a file and setting a custom context 290func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParameters) (file *File, err error) { 291 // Test if user token is valid. This helps because client.Do doesn't like this for some reason. XXX: More 292 // investigation needed, but for now this will do. 293 _, err = api.AuthTest() 294 if err != nil { 295 return nil, err 296 } 297 response := &fileResponseFull{} 298 values := url.Values{} 299 if params.Filetype != "" { 300 values.Add("filetype", params.Filetype) 301 } 302 if params.Filename != "" { 303 values.Add("filename", params.Filename) 304 } 305 if params.Title != "" { 306 values.Add("title", params.Title) 307 } 308 if params.InitialComment != "" { 309 values.Add("initial_comment", params.InitialComment) 310 } 311 if params.ThreadTimestamp != "" { 312 values.Add("thread_ts", params.ThreadTimestamp) 313 } 314 if len(params.Channels) != 0 { 315 values.Add("channels", strings.Join(params.Channels, ",")) 316 } 317 if params.Content != "" { 318 values.Add("content", params.Content) 319 values.Add("token", api.token) 320 err = api.postMethod(ctx, "files.upload", values, response) 321 } else if params.File != "" { 322 err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.File, "file", api.token, values, response, api) 323 } else if params.Reader != nil { 324 if params.Filename == "" { 325 return nil, fmt.Errorf("files.upload: FileUploadParameters.Filename is mandatory when using FileUploadParameters.Reader") 326 } 327 err = postWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.Filename, "file", api.token, values, params.Reader, response, api) 328 } 329 330 if err != nil { 331 return nil, err 332 } 333 334 return &response.File, response.Err() 335} 336 337// DeleteFileComment deletes a file's comment 338func (api *Client) DeleteFileComment(commentID, fileID string) error { 339 return api.DeleteFileCommentContext(context.Background(), fileID, commentID) 340} 341 342// DeleteFileCommentContext deletes a file's comment with a custom context 343func (api *Client) DeleteFileCommentContext(ctx context.Context, fileID, commentID string) (err error) { 344 if fileID == "" || commentID == "" { 345 return ErrParametersMissing 346 } 347 348 values := url.Values{ 349 "token": {api.token}, 350 "file": {fileID}, 351 "id": {commentID}, 352 } 353 _, err = api.fileRequest(ctx, "files.comments.delete", values) 354 return err 355} 356 357// DeleteFile deletes a file 358func (api *Client) DeleteFile(fileID string) error { 359 return api.DeleteFileContext(context.Background(), fileID) 360} 361 362// DeleteFileContext deletes a file with a custom context 363func (api *Client) DeleteFileContext(ctx context.Context, fileID string) (err error) { 364 values := url.Values{ 365 "token": {api.token}, 366 "file": {fileID}, 367 } 368 369 _, err = api.fileRequest(ctx, "files.delete", values) 370 return err 371} 372 373// RevokeFilePublicURL disables public/external sharing for a file 374func (api *Client) RevokeFilePublicURL(fileID string) (*File, error) { 375 return api.RevokeFilePublicURLContext(context.Background(), fileID) 376} 377 378// RevokeFilePublicURLContext disables public/external sharing for a file with a custom context 379func (api *Client) RevokeFilePublicURLContext(ctx context.Context, fileID string) (*File, error) { 380 values := url.Values{ 381 "token": {api.token}, 382 "file": {fileID}, 383 } 384 385 response, err := api.fileRequest(ctx, "files.revokePublicURL", values) 386 if err != nil { 387 return nil, err 388 } 389 return &response.File, nil 390} 391 392// ShareFilePublicURL enabled public/external sharing for a file 393func (api *Client) ShareFilePublicURL(fileID string) (*File, []Comment, *Paging, error) { 394 return api.ShareFilePublicURLContext(context.Background(), fileID) 395} 396 397// ShareFilePublicURLContext enabled public/external sharing for a file with a custom context 398func (api *Client) ShareFilePublicURLContext(ctx context.Context, fileID string) (*File, []Comment, *Paging, error) { 399 values := url.Values{ 400 "token": {api.token}, 401 "file": {fileID}, 402 } 403 404 response, err := api.fileRequest(ctx, "files.sharedPublicURL", values) 405 if err != nil { 406 return nil, nil, nil, err 407 } 408 return &response.File, response.Comments, &response.Paging, nil 409} 410