How to build a Discord message publisher
This recipe will help you to build a fully functional Discord webhook publisher and deploy it to Morty. The function will allow you to publish custom messages to a dedicated channel on a Discord server.
Prerequisites
For this recipe, you need to have access to an up and running Morty instance. Please refer to the deployment guide to deploy Morty on your own infrastructure. Also, you need to have the Morty CLI (opens in a new tab) installed on your machine. If you don't have already installed, you can run the following command to install it:
curl -fsSL https://morty-faas.github.io/install-cli.sh | sudo sh
We will use the Go (opens in a new tab) programming language for this recipe. The code will be explained so beginners will be able to follow this recipe.
Finally, you will need to have a valid Discord webhook URL. If you don't have this right now, please refer to the Discord official documentation to learn how to create a webhook (opens in a new tab).
Develop the function
Let's start our code session ! First of all, we will initialize a new blank function based on the Morty's Go runtime, go-1.19
:
morty fn init -r go-1.19 discord-webhook-publisher
Open the function folder discord-webhook-publisher
in your favorite IDE or code editor, and open the handler.go
file. You can remove all the boilerplate code present in the function Handler()
, we will start from a blank code. Your code should now look like :
package function
func Handler (w http.ResponseWriter, r *http.Request) {
// We will place our custom code here
}
Now, we need to create a constant for our webhook URL. Update the code to add a new constant, webhookUrl
:
package function
// Declare the constant here
const webhookUrl = "YOUR_WEBHOOK_URL"
func Handler (w http.ResponseWriter, r *http.Request) {}
We will now add some business logic into our function. The first step is to check the incoming HTTP request method. We want our function to work only if the HTTP method is POST. Also, the function will return an HTTP 204 No Content
if there are no errors.
package function
// Declare the constant here
const webhookUrl = "YOUR_WEBHOOK_URL"
func Handler (w http.ResponseWriter, r *http.Request) {
// If the incoming request is not POST,
// we return a HTTP 405 Method Not Allowed
if r.Method != http.MethodPost {
http.Error(w, "This function accept only POST requests.", http.StatusMethodNotAllowed)
return
}
// Send the HTTP 204 No Content
w.WriteHeader(http.StatusNoContent)
}
We now need to parse the request body to retrieve the message. The body the request will accept will have the following format:
{
"message": "The message to send to the Discord channel"
}
Let's add the piece of code allowing us to do that :
package function
// Declare the constant here
const webhookUrl = "YOUR_WEBHOOK_URL"
// Data Transfer Object for the request
type PublishMessageRequest struct {
Message string `json:"message"`
}
func Handler (w http.ResponseWriter, r *http.Request) {
// If the incoming request is not POST,
// we return a HTTP 405 Method Not Allowed
if r.Method != http.MethodPost {
http.Error(w, "This function accept only POST requests.", http.StatusMethodNotAllowed)
return
}
// Decode the request body, and if an error occurs,
// return a HTTP 400 Bad Request error
dto := &PublishMessageRequest{}
if err := json.NewDecoder(r.Body).Decode(dto); err != nil {
http.Error(w, fmt.Sprintf("Unable to decode request body: %v", err), http.StatusBadRequest)
return
}
// Send the HTTP 204 No Content
w.WriteHeader(http.StatusNoContent)
}
Seems right no ? The only things that is missing is the request to the webhook. Let's fix that :
package function
// Declare the constant here
const webhookUrl = "YOUR_WEBHOOK_URL"
// Data Transfer Object for the request
type PublishMessageRequest struct {
Message string `json:"message"`
}
func Handler (w http.ResponseWriter, r *http.Request) {
// If the incoming request is not POST,
// we return a HTTP 405 Method Not Allowed
if r.Method != http.MethodPost {
http.Error(w, "This function accept only POST requests.", http.StatusMethodNotAllowed)
return
}
// Decode the request body, and if an error occurs,
// return a HTTP 400 Bad Request error
dto := &PublishMessageRequest{}
if err := json.NewDecoder(r.Body).Decode(dto); err != nil {
http.Error(w, fmt.Sprintf("Unable to decode request body: %v", err), http.StatusBadRequest)
return
}
// Create a valid JSON request body for the webhook.
// https://discord.com/developers/docs/resources/webhook#execute-webhook
data, _ := json.Marshal(map[string]interface{}{
"user": "Morty",
"content": dto.Message,
})
// Create the request and execute it
req, _ := http.NewRequest(http.MethodPost, webhookUrl, bytes.NewReader(data))
req.Header.Add("Content-Type", "application/json")
if _, err := http.DefaultClient.Do(req); err != nil {
http.Error(w, fmt.Sprintf("failed to send data to webhook: %v", err), http.StatusInternalServerError)
return
}
// Send the HTTP 204 No Content
w.WriteHeader(http.StatusNoContent)
}
Et voilà ! Our code seems good and ready to work ! In the next section, we will deploy our function to a Morty instance, and invoke it !
Deploy the function
To deploy the function, it's a breeze. First ensure that your morty
CLI targets the good context using the following command :
morty config current-context
Ready ? Build your function !
morty fn build discord-webhook-publisher
Test our publisher
The long-awaited moment has arrived. After a lot of efforts, its time to try our function ! Let's start with the HTTP method verification. We want to be sure that we cannot send requests with other HTTP methods than POST
:
morty fn invoke discord-webhook-publisher -X GET
If everything is ok, you should see an error message This function accept only POST requests
.
So now, let's try to send a message :
morty fn invoke discord-webhook-publisher \
-X POST \
-H "Content-Type: application/json" \
-d '{"message": "Hello from my Discord publisher !"}'
On the Discord channel where the webhook was configured, you should have a message from Morty
:
Wrap up
Congratulations ! You just built a serverless function that can push messages to Discord. Maybe it sounds useless, but it had permit you to learn different things:
- Develop a serverless function in Go for Morty
- Build and deploy your function on a Morty instance
- Invoke your function with different parameters
The source code for this recipe is available on GitHub (opens in a new tab).