From 0c0ae0f39533714424e96c96b4004ed33be5dfd3 Mon Sep 17 00:00:00 2001 From: piekstra Date: Mon, 2 Mar 2026 08:41:16 -0500 Subject: [PATCH] feat(messages): include files field in message JSON output The Message struct was missing a Files field, so file attachment metadata from the Slack API (conversations.replies, conversations.history) was silently dropped during JSON unmarshalling. Messages with attachments appeared identical to plain text messages in JSON output. Add a File struct with the key fields (id, name, mimetype, filetype, size, url_private, url_private_download, permalink) and a Files field to Message so file data is preserved. Closes #119 --- internal/client/client.go | 14 +++++ internal/cmd/messages/messages_test.go | 73 ++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/internal/client/client.go b/internal/client/client.go index c9534dd..8ebbee8 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -230,6 +230,19 @@ type Reaction struct { Users []string `json:"users"` } +// File represents a file attachment on a Slack message +type File struct { + ID string `json:"id"` + Name string `json:"name"` + Title string `json:"title,omitempty"` + Mimetype string `json:"mimetype,omitempty"` + Filetype string `json:"filetype,omitempty"` + Size int64 `json:"size,omitempty"` + URLPrivate string `json:"url_private,omitempty"` + URLPrivateDownload string `json:"url_private_download,omitempty"` + Permalink string `json:"permalink,omitempty"` +} + // Message represents a Slack message type Message struct { Type string `json:"type"` @@ -239,6 +252,7 @@ type Message struct { ThreadTS string `json:"thread_ts,omitempty"` ReplyCount int `json:"reply_count,omitempty"` Reactions []Reaction `json:"reactions,omitempty"` + Files []File `json:"files,omitempty"` } // Team represents workspace info diff --git a/internal/cmd/messages/messages_test.go b/internal/cmd/messages/messages_test.go index 68e4f1d..b76fe23 100644 --- a/internal/cmd/messages/messages_test.go +++ b/internal/cmd/messages/messages_test.go @@ -543,6 +543,79 @@ func TestRunThread_JSONIncludesReactions(t *testing.T) { assert.Empty(t, messages[1].Reactions) } +func TestRunThread_JSONIncludesFiles(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/conversations.replies": + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "ok": true, + "messages": []map[string]interface{}{ + { + "ts": "1234567890.123456", + "user": "U001", + "text": "See the screenshot below", + "files": []map[string]interface{}{ + { + "id": "F0123ABC", + "name": "screenshot.png", + "title": "Screenshot", + "mimetype": "image/png", + "filetype": "png", + "size": 54321, + "url_private": "https://files.slack.com/files-pri/T123-F0123ABC/screenshot.png", + "url_private_download": "https://files.slack.com/files-pri/T123-F0123ABC/download/screenshot.png", + "permalink": "https://example.slack.com/files/U001/F0123ABC/screenshot.png", + }, + }, + }, + { + "ts": "1234567890.123457", + "user": "U002", + "text": "Plain reply without files", + }, + }, + }) + case "/users.info": + mockUserInfoHandler(w, r) + } + })) + defer server.Close() + + c := client.NewWithConfig(server.URL, "test-token", nil) + opts := &threadOptions{limit: 100} + + // Capture JSON output + output.OutputFormat = output.FormatJSON + defer func() { output.OutputFormat = output.FormatText }() + + var buf strings.Builder + output.Writer = &buf + defer func() { output.Writer = os.Stdout }() + + err := runThread("C123", "1234567890.123456", opts, c) + require.NoError(t, err) + + // Parse the JSON output + var messages []client.Message + err = json.Unmarshal([]byte(buf.String()), &messages) + require.NoError(t, err) + + require.Len(t, messages, 2) + + // First message should have files + require.Len(t, messages[0].Files, 1) + assert.Equal(t, "F0123ABC", messages[0].Files[0].ID) + assert.Equal(t, "screenshot.png", messages[0].Files[0].Name) + assert.Equal(t, "image/png", messages[0].Files[0].Mimetype) + assert.Equal(t, "png", messages[0].Files[0].Filetype) + assert.Equal(t, int64(54321), messages[0].Files[0].Size) + assert.Equal(t, "https://files.slack.com/files-pri/T123-F0123ABC/screenshot.png", messages[0].Files[0].URLPrivate) + assert.Equal(t, "https://example.slack.com/files/U001/F0123ABC/screenshot.png", messages[0].Files[0].Permalink) + + // Second message should have no files + assert.Empty(t, messages[1].Files) +} + func TestRunHistory_JSONIncludesReactions(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path {