Cross-posted from question on StackOverflow
According to the Discord.Net API Documentation page for the EmbedBuilder class, the syntax to add a local image to an EmbedBuilder object should look something like this (converted to VB):
```vb.net
Dim fileName = "image.png"
Dim embed = New EmbedBuilder() With {
.ImageUrl = $"attachment://{fileName}"
}.Build()
I'm trying to use something like this to add a dynamically created image to the `EmbedBuilder`, but I can't seem to get it to work properly. Here's basically what I've got:
```vb.net
Dim Builder As New Discord.EmbedBuilder
Dim DynamicImagePath As String = CreateDynamicImage()
Dim AttachURI As String = $"attachment:///" & DynamicImagePath.Replace("\", "/").Replace(" ", "%20")
With Builder
.Description = "SAMPLE DESCRIPTION"
.ImageUrl = AttachURI
End With
MyClient.GetGuild(ServerID).GetTextChannel(PostChannelID).SendMessageAsync("THIS IS A TEST", False, Builder.Build)
My CreateDynamicImage method returns the full path to the locally created image (e.g., C:\Folder\Another Folder\image.png). I've done a fair amount of "fighting"/testing with this to get past the Url must be a well-formed URI exception I was initially getting because of the [SPACE] in the path.
MyClient is a Discord.WebSocket.SocketClient object set elsewhere.
The SendMessageAsync method does send the embed to Discord on the correct channel, but without the embedded image.
If I instead send the image using the SendFileAsync method (like so):
vb.net
MyClient.GetGuild(ServerID).GetTextChannel(PostChannelID).SendFileAsync(DynamicImagePath, "THIS IS A TEST", False, Builder.Build)
the image is sent to Discord, but as a part of the message rather than included as a part of the embed (NOTE: this behavior is expected for the SendFileAsync method - I only mention it here because this was a part of my testing to ensure that there wasn't some sort of problem with actually sending the image to Discord).
I've tried using the file:/// scheme instead of the attachment:/// scheme, but that results in the entire post never making it to Discord at all.
Additionally, in other parts of my code, I can successfully set the ImageUrl property to a Web resource (e.g., https://www.somesite.com/someimage.png) and the embed looks exactly as expected with the image and everything when it successfully posts to Discord.
According to the only answer currently posted to the referenced StackOverflow question, it appears this may something to do with the CDN URL used by Discord.Net. - https://cdn.discordapp.com/attachments instead of the "newer"(?) address of https://media.discordapp.net. Obviously, if this is the case, I could try to rework and recompile the code for this bit, but as I have a fairly extensive amount of work yet to do, I believe this task is better suited to the library's author for investigation.
In the meantime, since using images that are already available with a Web URL seems to work as expected, I believe I have a solution that involves uploading the image to Imgur (using the Imgur.API NuGet library. Once the image is uploaded there, I should be able to use the generated link to populate the EmbedBuilder's ImageUrl property. I haven't had a chance to test this thoroughly yet, but based on my experience so far, I would assume that it should work.
The Embed (and EmbedImage) objects don't do anything with files. They simply pass the URI as configured straight into Discord. Discord then expects a URI in the form attachment://filename.ext if you want to refer to an attached image.
What you need to do is use SendFileAsync with the embed. You have two options here:
Use SendFileAsync with the Stream stream, string filename overload. I think this makes it clear what you need to do: you provide a file stream (via File.OpenRead or similar) and a filename. The provided filename does not have to match any file on disk. So, for example:
var embed = new EmbedBuilder()
.WithImageUrl("attachment://myimage.png")
.Build();
await channel.SendFileAsync(stream, "myimage.png", embed: embed);
Alternatively, you can use SendFileAsync with the string filePath overload. Internally, this gets a stream of the file at the path, and sets filename (as sent to Discord) to the last part of the path. So it's equivalent to:
using var stream = File.OpenRead(filePath);
var filename = Path.GetFileName(filePath);
await channel.SendFileAsync(stream, filename);
From here, you can see that if you want to use the string filePath overload, you need to set embed image URI to something like $"attachment://{Path.GetFileName(filePath)}", because the attachment filename must match the one sent to Discord..
Ahh. Apparently, I misunderstood the intention and usage of the method and property. As I understand what you're saying, the attachment scheme requires only the file name and not the entire path to the local image. Then, the image is uploaded as a Stream (or FileStream) to the Discord.WebSocket.SocketClient object via the SocketTextChannel (ISocketMessageChannel) object.
That all makes sense to me. I guess I thought the .ImageUrl property somehow "automatically" initiated a Stream in the background. However, when I try to implement these changes, I'm getting nothing sent to Discord at all. Perhaps I'm being dense here (a definite possibility), but here's what I've got:
```vb.net
Dim Builder As New Discord.EmbedBuilder
Dim AttachmentPath As String = CreateDynamicImage() '<-- Returns the full, local path to the created file
With Builder
.Description = "SAMPLE DESCRIPTION"
.ImageUrl = $"attachment://{IO.Path.GetFileName(AttachmentPath)}"
End With
Using AttachmentStream As IO.Stream = IO.File.OpenRead(AttachmentPath)
MyClient.GetGuild(ServerID).GetTextChannel(PostChannelID).SendFileAsync(AttachmentStream, IO.Path.GetFileName(AttachmentPath), "THIS IS A TEST", False, Builder.Build)
End Using
```
As it's an async method, you must await (or whatever the VB.NET equivalent is) on SendFileAsync.
What's happening with your code now:
using blockSendFileAsync (which initiates the send process on another thread)using block ends, stream gets disposedSendFileAsync at some point gets around to uploading the file from streamYou need to await on the async call so the dispose only happens after the call completes.
Alternatively use the filePath overload with the assumption that the filename that gets sent to Discord is the last part of the path (and therefore gets used with attachment://). In that situation, because Discord.Net internally opens the stream and manages the dispose, the lack of await doesn't impact the file send. But you should still always use await on all async calls anyway. I've now edited that into my previous comment where I omitted the awaits, sorry.
Side note, if you use the stream overload, the filename sent in attachment:// must match the filename sent during the file upload, but does not need to match the filename on disk. Discord never sees, and therefore never cares, what the local filename is.
Okay, so I was being dense. I actually thought that might be happening, but I hadn't gotten around to doing anything about it yet. Totally my fault. Thank you so much for your help. That makes me feel better.
Just a couple slightly more complete examples:
With filePath overload:
var filename = Path.GetFileName(filePath);
var embed = new EmbedBuilder()
.WithImageUrl($"attachment://{filename}")
.Build();
await channel.SendFileAsync(filePath: filePath, embed: embed, text: null);
With stream overload:
using var stream = File.OpenRead(filePath);
var filename = Path.GetFileName(filePath);
var embed = new EmbedBuilder()
.WithImageUrl($"attachment://{filename}")
.Build();
await channel.SendFileAsync(stream: stream, filename: filename, embed: embed, text: null);
With stream overload and an unrelated filename:
using var stream = File.OpenRead(filePath);
var filename = "image.png";
var embed = new EmbedBuilder()
.WithImageUrl($"attachment://{filename}")
.Build();
await channel.SendFileAsync(stream: stream, filename: filename, embed: embed, text: null);
Working great now, and I don't have to jump through the other hoops of uploading the image to another hosting site! Thanks SO much!
Looks like this has been solved. Closing.