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, &params, 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