File Accessibility Inconsistency: Java SDK vs Python SDK When Uploading Files Without Channel Specification
(Describe your issue and goal here) The Java SDK does not provide a way to:
- Upload a file using bot token without specifying a channel but still make it accessible for scheduling
- Upload a file using bot token to a channel without triggering an immediate notification
The Python SDK seems to handle case [1] correctly by just using the permalink, suggesting either:
There's an undocumented feature in the Python SDK There's a missing feature in the Java SDK There's a behavioral difference between the SDKs that should be documented
Environment Information
Java SDK version: 1.45.3 Python SDK version: 3.34.0 Bot token scopes: files:read, files:write
What I've tried
I tried uploading the content using requests to files.completeUploadExternal in Python and it successfully solved issue [1], but using Unirest in Java, I had to specify channelId to make the URL clickable.
I've also tried with filesuploadv2 method of Java SDK and files_upload_v2 method of Python SDK and the above behavior is replicated.
Also, tried with the deprecated files.upload method and it's still the same.
Questions
Is there an API call that allows a bot to upload a file and make it accessible without sending an immediate notification? Why does the Python SDK handle this differently than the Java SDK? Is there a way to suppress notification when uploading a file in the Java SDK similar to Python?
Requested Resolution Please provide a way in the Java SDK to upload a file and schedule it for later delivery without sending an immediate notification when the file is uploaded.
Category (place an x in each of the [ ])
- [ ] bolt (Bolt for Java)
- [ ] bolt-{sub modules} (Bolt for Java - optional modules)
- [x] slack-api-client (Slack API Clients)
- [ ] slack-api-model (Slack API Data Models)
- [ ] slack-api-*-kotlin-extension (Kotlin Extensions for Slack API Clients)
- [ ] slack-app-backend (The primitive layer of Bolt for Java)
Requirements
Please make sure if this topic is specific to this SDK. For general questions/issues about Slack API platform or its server-side, could you submit questions at https://my.slack.com/help/requests/new instead. :bow:
Please read the Contributing guidelines and Code of Conduct before creating this issue or pull request. By submitting, you agree to those rules.
Hey @stevesmith-savvy! π Thanks for sharing these questions and findings π
I hope the following can help, but I'm also curious about the setups being used. More on this shall follow below:
- Upload a file using bot token without specifying a channel but still make it accessible for scheduling
The following snippet is giving me an option to post a message with the file after uploading it, which I'm hoping can be useful for scheduling a post later:
FilesUploadV2Response v2Response = slack.methods(token).filesUploadV2(r -> r
.file(new java.io.File(filepath)));
File v2File = v2Response.getFile();
ChatPostMessageResponse messageResponse = slack.methods(token).chatPostMessage(r -> r
.channel(channel)
.text(v2File.getPermalink()));
Using the same token is an important note here since this is required to share uploaded files to a channel π
- Upload a file using bot token to a channel without triggering an immediate notification
AFAICT omitting the channel attribute of the filesUploadV2 request prevents notifications altogether, though I'm wondering if a different setup is being for this?
Please let me know if the snippet above is helpful or if additional investigations are needed! Uploading files can have edges that I'm glad to help with for shared understandings π€ β¨
Thanks for looking into this @zimeg!
AFAICT omitting the channel attribute of the filesUploadV2 request prevents notifications altogether, though I'm wondering if a different setup is being for this?
Yes, omitting the channel prevents notifications, but this creates another problem in Java: the permalink becomes non-clickable/inaccessible to users in scheduled messages.
This is the core inconsistency:
- In Python: We can upload without a channel, and when we use the permalink in a scheduled message, it works fine
- In Java: When we upload without a channel, the permalink in scheduled / post messages doesn't work for users or to the intended user in DM.
I notice the documentation also states the same: In most cases, callers will supply a channel where the file will be shared. If the channel_id is not specified, the file will remain private..
Attaching snippets that I've used for reference:
FilesUploadV2Response uploadResponse =
this.slackMethodsClient.filesUploadV2(
FilesUploadV2Request.builder()
.token(TOKEN)
.channel(channelId)
.fileData(contentBytes)
.title(title)
.initialComment(title)
.build());
if (!uploadResponse.isOk()) {
return null;
}
return uploadResponse.getFiles().get(0).getPermalink();
Using the same token is an important note here since this is required to share uploaded files to a channel π
Yes, the same token is used for uploading and posting a message. My apologies if that wasn't inferred from the above message.
@stevesmith-savvy For sure! But I'm having trouble finding the same behaviors right now...
The following is working as expected for me and unfurls the posted file in channel after a few seconds:
FilesUploadV2Response v2Response = slack.methods(token).filesUploadV2(r -> r
.file(new java.io.File(filepath)));
File v2File = v2Response.getFile();
int epoch = (int) java.time.Instant.now().getEpochSecond();
ChatScheduleMessageResponse scheduleResponse = slack.methods(token).chatScheduleMessage(r -> r
.channel(channel)
.text(v2File.getPermalink())
.postAt(epoch + 10));
AFAICT the same URL is returned with both permalink methods and a single file too:
- v2Response.getFiles().get(0).getPermalink();
+ v2File.getFile().getPermalink();
Might it be possible that the uploading bot isn't a member of the channel it's uploading to? AFAICT this shouldn't cause issues if the chat.write:public scope is added - the message would not post otherwise - but this might somehow affect file permissions. FWIW this isn't causing issues for me though.
This is true, but posting a message with the file permalink and the same token makes this file shared. Using the same token makes this possible, so thanks for confirming this!
I'd next like to check that the filesUploadV2 snippet with a channel attribute posts the file as expected? The behavior of file sharing should be identical across all SDKs so this might be a difference in implementation π€
I have an audio file that I want to upload to Slack and include as a clickable permalink in a Canvas.
If I set the channelId in the FilesUpload request, the bot immediately sends a notification with the shared file. Then, at the scheduled time, it sends another message containing the Canvas link. In this case, the audio link inside the Canvas is clickable.
However, if I don't set the channelId, the bot doesn't send any immediate notification. It only sends the message with the Canvas at the scheduled timeβbut in this case, the audio link inside the Canvas is not clickable.
Here's the entire code for your reference:
public class SlackCanvasMessageScheduler {
private final Slack slack;
private final String botToken;
public SlackCanvasMessageScheduler(String botToken) {
this.slack = Slack.getInstance();
this.botToken = botToken;
}
/**
* Uploads an audio file to Slack and returns the file ID
*/
public String uploadAudioFile(String filePath, String channelId) {
try {
File audioFile = new File(filePath);
if (!audioFile.exists()) {
throw new RuntimeException("Audio file not found: " + filePath);
}
// Read file bytes
byte[] fileBytes = Files.readAllBytes(Paths.get(filePath));
// Create file upload request
FilesUploadV2Request request = FilesUploadV2Request.builder()
.token(botToken)
.filename(audioFile.getName())
.fileData(fileBytes)
.initialComment("Audio file for canvas")
// .channels(List.of(channelId))
.build();
// Upload file
FilesUploadV2Response response = slack.methods(botToken).filesUploadV2(request);
if (response.isOk() && response.getFiles() != null && !response.getFiles().isEmpty()) {
String fileUrl = response.getFiles().get(0).getPermalink();
System.out.println("Audio file uploaded successfully with url: " + fileUrl);
return fileUrl;
} else {
throw new RuntimeException("Failed to upload audio file: " + response.getError());
}
} catch (IOException e) {
System.err.println("Error reading audio file: " + e.getMessage());
throw new RuntimeException(e);
} catch (Exception e) {
System.err.println("Error uploading audio file: " + e.getMessage());
throw new RuntimeException(e);
}
}
/**
* Creates a canvas in Slack with text content and embedded audio
*/
public String createCanvasWithAudio(String title, String textContent, String audioFileUrl) {
try {
// Create canvas content with embedded audio
String canvasContent = createCanvasContentWithAudio(textContent, audioFileUrl);
System.out.printf("Canvas content: %s%n", canvasContent);
CanvasDocumentContent documentContent = new CanvasDocumentContent();
documentContent.setMarkdown(canvasContent);
// Create canvas request
CanvasesCreateRequest request = CanvasesCreateRequest.builder()
.token(botToken)
.title(title)
.documentContent(documentContent)
.build();
// Execute canvas creation
CanvasesCreateResponse response = slack.methods(botToken).canvasesCreate(request);
if (response.isOk()) {
String canvasId = response.getCanvasId();
System.out.println("Canvas with audio created successfully with ID: " + canvasId);
return canvasId;
} else {
throw new RuntimeException("Failed to create canvas: " + response.getError());
}
} catch (Exception e) {
System.err.println("Error creating canvas with audio: " + e.getMessage());
throw new RuntimeException(e);
}
}
/**
* Creates canvas markdown content with embedded audio
*/
private String createCanvasContentWithAudio(String textContent, String audioFileUrl) {
StringBuilder content = new StringBuilder();
// Add text content
if (textContent != null && !textContent.isEmpty()) {
content.append(textContent).append("\n\n");
}
// Add audio embed - Slack uses file ID reference
content.append("## Audio Content\n\n");
content.append("Tune in to the audio [here](").append(audioFileUrl).append(")\n\n");
return content.toString();
}
/**
* Creates a canvas in Slack with simple text content (original method preserved)
*/
public String createCanvas(String title, String textContent) {
try {
// Create canvas content
CanvasDocumentContent documentContent = new CanvasDocumentContent();
documentContent.setMarkdown(textContent);
// Create canvas request
CanvasesCreateRequest request = CanvasesCreateRequest.builder()
.token(botToken)
.title(title)
.documentContent(documentContent)
.build();
// Execute canvas creation
CanvasesCreateResponse response = slack.methods(botToken).canvasesCreate(request);
if (response.isOk()) {
String canvasId = response.getCanvasId();
System.out.println("Canvas created successfully with ID: " + canvasId);
return canvasId;
} else {
throw new RuntimeException("Failed to create canvas: " + response.getError());
}
} catch (Exception e) {
System.err.println("Error creating canvas: " + e.getMessage());
throw new RuntimeException(e);
}
}
/**
* Sets read access permissions for the canvas
*/
public void setCanvasReadPermissions(String canvasId, List<String> userIds) {
try {
CanvasesAccessSetRequest request = CanvasesAccessSetRequest.builder()
.token(botToken)
.canvasId(canvasId)
.accessLevel("read")
.userIds(userIds)
.build();
CanvasesAccessSetResponse response = slack.methods(botToken).canvasesAccessSet(request);
if (response.isOk()) {
System.out.println("Canvas read permissions set successfully");
} else {
throw new RuntimeException("Failed to set canvas permissions: " + response.getError());
}
} catch (Exception e) {
System.err.println("Error setting canvas permissions: " + e.getMessage());
throw new RuntimeException(e);
}
}
/**
* Creates the canvas URL
*/
public String createCanvasUrl(String domain, String workspaceId, String canvasId) {
return String.format("https://%s.slack.com/docs/%s/%s", domain, workspaceId, canvasId);
}
/**
* Schedules a message with canvas URL to be sent in 10 seconds
*/
public void scheduleMessageWithCanvas(String channelId, String domain, String workspaceId, String canvasId, String messageText) {
try {
String canvasUrl = createCanvasUrl(domain, workspaceId, canvasId);
// Calculate post time (10 seconds from now)
long postAt = Instant.now().getEpochSecond() + 10;
// Create message with canvas URL
String fullMessage = messageText + "\n\nCanvas: " + canvasUrl;
ChatScheduleMessageRequest request = ChatScheduleMessageRequest.builder()
.token(botToken)
.channel(channelId)
.text(fullMessage)
.postAt((int)postAt)
.build();
ChatScheduleMessageResponse response = slack.methods(botToken).chatScheduleMessage(request);
if (response.isOk()) {
System.out.println("Message scheduled successfully!");
System.out.println("Scheduled message ID: " + response.getScheduledMessageId());
System.out.println("Will be sent at: " + Instant.ofEpochSecond(postAt));
System.out.println("Canvas URL: " + canvasUrl);
} else {
throw new RuntimeException("Failed to schedule message: " + response.getError());
}
} catch (Exception e) {
System.err.println("Error scheduling message: " + e.getMessage());
throw new RuntimeException(e);
}
}
/**
* Complete workflow: Upload audio, create canvas with audio, set permissions, and schedule message
*/
public void createCanvasWithAudioAndScheduleMessage(String channelId, String canvasTitle,
String canvasContent, String audioFilePath, String messageText, String domain,
String workspaceId, List<String> userIds) {
try {
System.out.println("Starting audio upload, canvas creation and message scheduling...");
// Step 1: Upload audio file
String audioFileUrl = uploadAudioFile(audioFilePath, channelId);
// Step 2: Create canvas with audio
String canvasId = createCanvasWithAudio(canvasTitle, canvasContent, audioFileUrl);
// Step 3: Set read permissions
setCanvasReadPermissions(canvasId, userIds);
// Step 4: Schedule message with canvas URL
scheduleMessageWithCanvas(channelId, domain, workspaceId, canvasId, messageText);
System.out.println("Process completed successfully!");
} catch (Exception e) {
System.err.println("Error in complete workflow with audio: " + e.getMessage());
throw new RuntimeException(e);
}
}
}
@stevesmith-savvy This is so helpful! Thanks for sharing this setup π β¨
If I set the channelId in the FilesUpload request, the bot immediately sends a notification with the shared file. Then, at the scheduled time, it sends another message containing the Canvas link. In this case, the audio link inside the Canvas is clickable.
However, if I don't set the channelId, the bot doesn't send any immediate notification. It only sends the message with the Canvas at the scheduled timeβbut in this case, the audio link inside the Canvas is not clickable.
The problem to me seems to be including the uploaded file permalink in the canvas and posting a link to the canvas to a channel, but not making the file share explicit in the second case.
While not ideal, this seems to match the current expected behavior, although I understand too that this might be a product gap.
- Upload a file using bot token without specifying a channel but still make it accessible for scheduling
...
I tried uploading the content using requests to
files.completeUploadExternalin Python and it successfully solved issue [1], but using Unirest in Java, I had to specify channelId to make the URL clickable.
It wasn't clear at first that this issue seems to be appearing with canvases, but for confidence was the file uploaded without a channel ID using Python shared alright using a canvas?
Also, not meaning to skip over this earlier question:
Is there an API call that allows a bot to upload a file and make it accessible without sending an immediate notification?
The files.sharedPublicURL method might be useful, but I have not tried this with the setup above at this time.
Let me know if some of this seems to be pointing at a problem somewhere specific since we might want to mark this as a server side issue if unexpected differences are being found πΎ
The files.sharedPublicURL method might be useful, but I have not tried this with the setup above at this time.
Yes, I have already explored this and the documentation mentions it requires user tokens, which I don't think I can obtain.
π It looks like this issue has been open for 30 days with no activity. We'll mark this as stale for now, and wait 10 days for an update or for further comment before closing this issue out. If you think this issue needs to be prioritized, please comment to get the thread going again! Maintainers also review issues marked as stale on a regular basis and comment or adjust status if the issue needs to be reprioritized.
As this issue has been inactive for more than one month, we will be closing it. Thank you to all the participants! If you would like to raise a related issue, please create a new issue which includes your specific details and references this issue number.