Skip to content

Commit 34db83d

Browse files
localai-botlocalai-bot
andauthored
fix(slack): correct thread timestamp and improve file upload handling (#447)
Root cause: uploadJobResultFiles was passing msgTs (placeholder reply timestamp) as thread_ts for file uploads. Slack's API rejects this - it requires the parent thread timestamp, not a reply's timestamp. Changes: 1. Use 'ts' (thread root timestamp) instead of 'msgTs' in replyToUpdateMessage 2. Fix type handling in attachmentsFromMetadataOnly to handle []interface{} 3. Download and upload generated images (e.g., DALL-E) as files instead of just adding link attachments, to preserve temporary URLs 4. Remove dead code: generateAttachmentsFromJobResponse was never called This matches Telegram connector behavior where files are properly uploaded rather than just referenced by URL. Co-authored-by: localai-bot <localai-bot@noreply.github.com>
1 parent 5a27c47 commit 34db83d

File tree

1 file changed

+50
-27
lines changed

1 file changed

+50
-27
lines changed

services/connectors/slack.go

Lines changed: 50 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import (
44
"bytes"
55
"encoding/base64"
66
"fmt"
7+
"io"
78
"log"
9+
"net/http"
810
"os"
911
"path/filepath"
1012
"strings"
@@ -176,25 +178,21 @@ func attachmentsFromMetadataOnly(metadata map[string]interface{}) (attachments [
176178
return nil
177179
}
178180
if urls, exists := metadata[actions.MetadataUrls]; exists {
179-
if sl, ok := urls.([]string); ok {
180-
for _, url := range xstrings.UniqueSlice(sl) {
181-
attachments = append(attachments, slack.Attachment{
182-
Title: "URL",
183-
TitleLink: url,
184-
Text: url,
185-
})
186-
}
181+
for _, url := range xstrings.UniqueSlice(stringSliceFromMetadata(urls)) {
182+
attachments = append(attachments, slack.Attachment{
183+
Title: "URL",
184+
TitleLink: url,
185+
Text: url,
186+
})
187187
}
188188
}
189189
if imagesUrls, exists := metadata[actions.MetadataImages]; exists {
190-
if sl, ok := imagesUrls.([]string); ok {
191-
for _, url := range xstrings.UniqueSlice(sl) {
192-
attachments = append(attachments, slack.Attachment{
193-
Title: "Image",
194-
TitleLink: url,
195-
ImageURL: url,
196-
})
197-
}
190+
for _, url := range xstrings.UniqueSlice(stringSliceFromMetadata(imagesUrls)) {
191+
attachments = append(attachments, slack.Attachment{
192+
Title: "Image",
193+
TitleLink: url,
194+
ImageURL: url,
195+
})
198196
}
199197
}
200198
return attachments
@@ -295,6 +293,39 @@ func uploadFilesFromMetadata(metadata map[string]interface{}, api *slack.Client,
295293
}
296294
}
297295
}
296+
// Handle generated images (download from URL and upload as file, so temporary URLs like DALL-E are preserved)
297+
if imageUrls, exists := metadata[actions.MetadataImages]; exists {
298+
sl := stringSliceFromMetadata(imageUrls)
299+
for _, imgURL := range xstrings.UniqueSlice(sl) {
300+
resp, err := http.Get(imgURL)
301+
if err != nil {
302+
xlog.Error("Error downloading image for Slack upload", "url", imgURL, "error", err)
303+
continue
304+
}
305+
data, err := io.ReadAll(resp.Body)
306+
resp.Body.Close()
307+
if err != nil {
308+
xlog.Error("Error reading image body for Slack upload", "url", imgURL, "error", err)
309+
continue
310+
}
311+
if len(data) == 0 {
312+
xlog.Error("Empty image body for Slack upload", "url", imgURL)
313+
continue
314+
}
315+
_, err = api.UploadFileV2(slack.UploadFileV2Parameters{
316+
Reader: bytes.NewReader(data),
317+
FileSize: len(data),
318+
ThreadTimestamp: threadTs,
319+
Channel: channelID,
320+
Filename: "image.png",
321+
Title: "Generated image",
322+
InitialComment: "Generated image",
323+
})
324+
if err != nil {
325+
xlog.Error("Slack UploadFileV2 failed for image", "error", err, "url", imgURL)
326+
}
327+
}
328+
}
298329
}
299330

300331
// attachmentsAndUploadsFromMetadata returns link/image attachments and uploads files (songs, PDFs)
@@ -329,16 +360,6 @@ func uploadJobResultFiles(res *types.JobResult, api *slack.Client, channelID, th
329360
}
330361
}
331362

332-
func generateAttachmentsFromJobResponse(j *types.JobResult, api *slack.Client, channelID, ts string) (attachments []slack.Attachment) {
333-
if j == nil {
334-
return nil
335-
}
336-
for _, state := range j.State {
337-
attachments = append(attachments, attachmentsAndUploadsFromMetadata(state.Metadata, api, channelID, ts)...)
338-
}
339-
return attachments
340-
}
341-
342363
// ImageData represents a single image with its metadata
343364
type ImageData struct {
344365
Data []byte
@@ -641,7 +662,9 @@ func replyWithPostMessage(finalResponse string, api *slack.Client, ev *slackeven
641662

642663
func replyToUpdateMessage(finalResponse string, api *slack.Client, ev *slackevents.AppMentionEvent, msgTs string, ts string, postMessageParams slack.PostMessageParameters, res *types.JobResult) {
643664
attachments := attachmentsFromJobResponseOnly(res)
644-
uploadJobResultFiles(res, api, ev.Channel, msgTs)
665+
// Use the thread root timestamp (ts), not the placeholder reply timestamp (msgTs).
666+
// Slack API: "Never use a reply's ts value; use its parent instead."
667+
uploadJobResultFiles(res, api, ev.Channel, ts)
645668
if len(finalResponse) > 3000 {
646669
messages := xstrings.SplitParagraph(finalResponse, 3000)
647670

0 commit comments

Comments
 (0)