Skip to content

Commit e1504b0

Browse files
authored
feat(messages): include files field in message JSON output (#120)
## Summary - Add `File` struct and `Files` field to `Message` so file attachment metadata from the Slack API is preserved in JSON output - Covers `messages thread`, `messages history`, and any other command returning `Message` objects ## Why The `Message` struct had no `Files` field, so file attachment data returned by the Slack API (`conversations.replies`, `conversations.history`) was silently dropped during JSON unmarshalling. A message like "see the screenshot below" appeared identical to a plain text message in `--output json`, losing important context for automated consumers. ## Test plan - [x] New test `TestRunThread_JSONIncludesFiles` verifies files round-trip through JSON output - [x] Existing tests pass (reactions, history, thread) - [x] Linter clean (`0 issues`) Closes #119
1 parent 4e13edd commit e1504b0

2 files changed

Lines changed: 87 additions & 0 deletions

File tree

internal/client/client.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,19 @@ type Reaction struct {
230230
Users []string `json:"users"`
231231
}
232232

233+
// File represents a file attachment on a Slack message
234+
type File struct {
235+
ID string `json:"id"`
236+
Name string `json:"name"`
237+
Title string `json:"title,omitempty"`
238+
Mimetype string `json:"mimetype,omitempty"`
239+
Filetype string `json:"filetype,omitempty"`
240+
Size int64 `json:"size,omitempty"`
241+
URLPrivate string `json:"url_private,omitempty"`
242+
URLPrivateDownload string `json:"url_private_download,omitempty"`
243+
Permalink string `json:"permalink,omitempty"`
244+
}
245+
233246
// Message represents a Slack message
234247
type Message struct {
235248
Type string `json:"type"`
@@ -239,6 +252,7 @@ type Message struct {
239252
ThreadTS string `json:"thread_ts,omitempty"`
240253
ReplyCount int `json:"reply_count,omitempty"`
241254
Reactions []Reaction `json:"reactions,omitempty"`
255+
Files []File `json:"files,omitempty"`
242256
}
243257

244258
// Team represents workspace info

internal/cmd/messages/messages_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,79 @@ func TestRunThread_JSONIncludesReactions(t *testing.T) {
543543
assert.Empty(t, messages[1].Reactions)
544544
}
545545

546+
func TestRunThread_JSONIncludesFiles(t *testing.T) {
547+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
548+
switch r.URL.Path {
549+
case "/conversations.replies":
550+
_ = json.NewEncoder(w).Encode(map[string]interface{}{
551+
"ok": true,
552+
"messages": []map[string]interface{}{
553+
{
554+
"ts": "1234567890.123456",
555+
"user": "U001",
556+
"text": "See the screenshot below",
557+
"files": []map[string]interface{}{
558+
{
559+
"id": "F0123ABC",
560+
"name": "screenshot.png",
561+
"title": "Screenshot",
562+
"mimetype": "image/png",
563+
"filetype": "png",
564+
"size": 54321,
565+
"url_private": "https://files.slack.com/files-pri/T123-F0123ABC/screenshot.png",
566+
"url_private_download": "https://files.slack.com/files-pri/T123-F0123ABC/download/screenshot.png",
567+
"permalink": "https://example.slack.com/files/U001/F0123ABC/screenshot.png",
568+
},
569+
},
570+
},
571+
{
572+
"ts": "1234567890.123457",
573+
"user": "U002",
574+
"text": "Plain reply without files",
575+
},
576+
},
577+
})
578+
case "/users.info":
579+
mockUserInfoHandler(w, r)
580+
}
581+
}))
582+
defer server.Close()
583+
584+
c := client.NewWithConfig(server.URL, "test-token", nil)
585+
opts := &threadOptions{limit: 100}
586+
587+
// Capture JSON output
588+
output.OutputFormat = output.FormatJSON
589+
defer func() { output.OutputFormat = output.FormatText }()
590+
591+
var buf strings.Builder
592+
output.Writer = &buf
593+
defer func() { output.Writer = os.Stdout }()
594+
595+
err := runThread("C123", "1234567890.123456", opts, c)
596+
require.NoError(t, err)
597+
598+
// Parse the JSON output
599+
var messages []client.Message
600+
err = json.Unmarshal([]byte(buf.String()), &messages)
601+
require.NoError(t, err)
602+
603+
require.Len(t, messages, 2)
604+
605+
// First message should have files
606+
require.Len(t, messages[0].Files, 1)
607+
assert.Equal(t, "F0123ABC", messages[0].Files[0].ID)
608+
assert.Equal(t, "screenshot.png", messages[0].Files[0].Name)
609+
assert.Equal(t, "image/png", messages[0].Files[0].Mimetype)
610+
assert.Equal(t, "png", messages[0].Files[0].Filetype)
611+
assert.Equal(t, int64(54321), messages[0].Files[0].Size)
612+
assert.Equal(t, "https://files.slack.com/files-pri/T123-F0123ABC/screenshot.png", messages[0].Files[0].URLPrivate)
613+
assert.Equal(t, "https://example.slack.com/files/U001/F0123ABC/screenshot.png", messages[0].Files[0].Permalink)
614+
615+
// Second message should have no files
616+
assert.Empty(t, messages[1].Files)
617+
}
618+
546619
func TestRunHistory_JSONIncludesReactions(t *testing.T) {
547620
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
548621
switch r.URL.Path {

0 commit comments

Comments
 (0)