DEV Community

Elito
Elito

Posted on

Attaching Images to Discord Webhooks with Google App Scripts

Hey there!
Ever wanted to write a discord bot to send automated messages to your discord community? Google App Script's might be the cheaper solution for you! You can send messages on a schedule, fetch data, even save data. However, you can't directly interact with the user, so commands, chat messages, user role changes, etc. won't work. For example, I use App Scripts to automatically send me notifications for Google form submissions, send me daily digests of my projects. And there's so much more possible!

Google App Script Basics

Google App Scripts is a google service that allows you to automate various tasks, hook into other Google Apps like sheets, forms or drive. There are tons of options, so check out their docs if you're curious! You'll write your code in Google Script, a superset of JavaScript, which for the most part only adds syntax support for the various google services.

Checking out a basic Discord Webhook example

Let's start by creating a new App Script project!
New App Script Project Button
Give your project a name and let's get started!
Empty project Window
Let's start by familiarizing us with the Editor. As soon as we save out code, we're ready to Run it. This will open up a console telling us how long the execution took.
Great, now let's get that discord Webhook going! Right click a channel and Edit it. Under Integration create a new Webhook and copy it's URL.
Integration View Discord
Copy Webhook URL window Discord
Neat! Now we can build our message!

function myFunction() {

  const options = {
        "muteHttpExceptions": true,
        "method": "post",
        "contentType": "application/json",
        "payload": JSON.stringify({
        "content" : null,
        "embeds" : [{
          "color" : 261331,
          "title" : "**Webhook Demonstration**",
          "url" : "https://dev.to/elitogame",
          "fields" : [
            {
              name: "Greeting",
              value: "Hello there!"
            },
            {
              name: "Response",
              value: "General Kenobi! You're a bold one."
            }
          ]
        }]
      })
    };
    UrlFetchApp.fetch("https://discord.com/api/webhooks/1166047434766557194/JpKfdsaXyTPaliZB5U0B-GBQU0YtHaLMtf7mtM8ryv3Targ-ImKmhlyrzv5UXU98OPcq", options);
}
Enter fullscreen mode Exit fullscreen mode

If we try to run the myFunction function, Google will ask us to authorize the script. Click through the dialogues and accept them. You may have to click the Advanced option to continue too. Once we've authorized our script, we'll get the following webhook message on discord:
Initial Webhook Result

Great, now you could go on and customize it further. You can use e.g. this generator to build a JSON representation of your embed, or you just use the discord docs!

We're using UrlFetchApp to send the embed. It's a handy class, that can also be used, if you need to fetch data from an API.

Attach Images

Now, let's look at why I wanted to write this blog in the first place! Using images from the web, with a valid URL work without problems, but if you load images from other google services like Drive or convert Charts to images, you'll run into a barrier.

Here, you'll have to change a bunch of stuff to make this work:

function myFunction() {

  // Create the chart
  let chart = Charts.newLineChart();
  const datatable = Charts.newDataTable();
  // Columns - x axis & labels
  datatable.addColumn(Charts.ColumnType.STRING, "Date")
    .addColumn(Charts.ColumnType.NUMBER, "Object1")
    .addColumn(Charts.ColumnType.NUMBER, "Object2");

  // x axis values & y values for the 2 labels.
  datatable.addRow(["21.10.2023", 1, 4])
  datatable.addRow(["22.10.2023", 4, 5])
  datatable.addRow(["23.10.2023", 5, 2])

  // Create the chart and get an image from it.
  // Then conver the image into a base64 image.
  const data = datatable.build();
  chart.setDataTable(data);
  const imageData = Utilities.base64Encode(chart.build().getAs('image/png').getBytes());

  const options = {
        "muteHttpExceptions": true,
        "method": "post",
        "contentType": "multipart/form-data; boundary=---011000010111000001101001",
        "payload": `
-----011000010111000001101001
Content-Disposition: form-data; name="payload_json"
Content-Type: application/json

` + JSON.stringify({
        "content" : null,
        "embeds" : [{
          "color" : 261331,
          "title" : "**Webhook Demonstration**",
          "url" : "https://dev.to/elitogame",
          "fields" : [
            {
              name: "Greeting",
              value: "Hello there!"
            },
            {
              name: "Response",
              value: "General Kenobi! You're a bold one."
            }
          ],
          "image": {
            "url": "attachment://myfilename.png"
          }
        }]
      }) + `
-----011000010111000001101001
Content-Disposition: form-data; name="files[0]";filename="myfilename.png"
Content-Type: image/png
Content-transfer-encoding: base64

${imageData}
-----011000010111000001101001--`
    };
    UrlFetchApp.fetch("https://discord.com/api/webhooks/1166047434766557194/JpKfdsaXyTPaliZB5U0B-GBQU0YtHaLMtf7mtM8ryv3Targ-ImKmhlyrzv5UXU98OPcq", options);
}
Enter fullscreen mode Exit fullscreen mode

First up, you'll want to change the contentType to "multipart/form-data; boundary=---011000010111000001101001". This allows us to upload images. The boundary is used to split the different form parts from each other.

This new content type obviously also means, we've got to restructure our payload! The boundary splits up the payload in multiple parts, which each can have a header and content. In our first part, we define the json content, like before. We also have to add the appropriate headers:

Content-Disposition: form-data; name="payload_json"
Content-Type: application/json
Enter fullscreen mode Exit fullscreen mode

The content of the second part is our attachment! Our images should be encoded in Data URI scheme, which we will go over soon. The header for our images looks a bit different:

Content-Disposition: form-data; name="files[0]"; filename="myfilename.png"
Content-Type: image/png
Content-transfer-encoding: base64
Enter fullscreen mode Exit fullscreen mode

The name must be files[n], where n is the index of the attachment. Just add more form parts for each image, should you need more than 1 attachment. Just keep in mind that your image file sizes must be below 25MB per attachment. the filename can be anything, but it must be unique, so you can reference it later and the file ending should match up with the content you are providing (e.g.: .png, .jpg, .gif). With that being said, Content-Type should obviously also reflect the content you want to show! Next up, we need to set the Content-transfer-encoding to base64, otherwise the images will not render, as your files are going to be a plain base64 text file with a .png file extension. Below the headers, we'll put the base64 image data.

Let's look at the images. We're generating a basic Line Chart, get it as a png image, get the image's bytes, encode these into base64 and we've got our image!

  // Create the chart
  let chart = Charts.newLineChart();
  const datatable = Charts.newDataTable();
  // Columns - x axis & labels
  datatable.addColumn(Charts.ColumnType.STRING, "Date")
    .addColumn(Charts.ColumnType.NUMBER, "Object1")
    .addColumn(Charts.ColumnType.NUMBER, "Object2");

  // x axis values & y values for the 2 labels.
  datatable.addRow(["21.10.2023", 1, 4])
  datatable.addRow(["22.10.2023", 4, 5])
  datatable.addRow(["23.10.2023", 5, 2])

  // Create the chart and get an image from it.
  // Then conver the image into a base64 image.
  const data = datatable.build();
  chart.setDataTable(data);
  const imageData = Utilities.base64Encode(chart.build().getAs('image/png').getBytes());
Enter fullscreen mode Exit fullscreen mode

Now that should already attach our image to the message! If we want to embed the image into the embed message, we'll have to add it via:

"image": {
  "url": "attachment://myfilename.png"
}
Enter fullscreen mode Exit fullscreen mode

in the embed object! The URL schema is via attachment://[IMAGENAME], where the IMAGENAME is the filename we've defined earlier in the other form part.

Conclusion

We've looked at how to create a google app script and how to send discord embeds with images attached. Now it's up to you how you wanna automate your workflow!

Final discord message

Top comments (0)