Skip to content

Commit

Permalink
Merge pull request #296 from appwrite/feat-go-discord-command-bot
Browse files Browse the repository at this point in the history
Feat: Go discord command bot
  • Loading branch information
Meldiron authored Jul 24, 2024
2 parents 6d251a9 + e342846 commit 7779c32
Show file tree
Hide file tree
Showing 10 changed files with 319 additions and 7 deletions.
2 changes: 2 additions & 0 deletions go/discord-command-bot/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Directory used by Appwrite CLI for local development
.appwrite
81 changes: 81 additions & 0 deletions go/discord-command-bot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# 🤖 Go Discord Command Bot Function

Simple command using Discord Interactions.

## 🧰 Usage

### POST /interactions

Webhook to receive Discord command events. To receive events, you must register your application as a [Discord bot](https://discord.com/developers/applications).

**Parameters**

| Name | Description | Location | Type | Sample Value |
| --------------------- | -------------------------------- | -------- | ------ | --------------------------------------------------------------------------------------------- |
| x-signature-ed25519 | Signature of the request payload | Header | string | `d1efb...aec35` |
| x-signature-timestamp | Timestamp of the request payload | Header | string | `1629837700` |
| JSON Body | GitHub webhook payload | Body | Object | See [Discord docs](https://discord.com/developers/docs/interactions/receiving-and-responding) |

**Response**

Sample `200` Response:

Returns a Discord message object.

```json
{
"type": 4,
"data": {
"content": "Hello from Appwrite 👋"
}
}
```

Sample `401` Response:

```json
{
"error": "Invalid request signature"
}
```

## ⚙️ Configuration

| Setting | Value |
| ----------------- | ------------- |
| Runtime | Go (1.22) |
| Entrypoint | `main.go` |
| Permissions | `any` |
| Timeout (Seconds) | 15 |

## 🔒 Environment Variables

### DISCORD_PUBLIC_KEY

Public Key of your application in Discord Developer Portal.

| Question | Answer |
| ------------- | ---------------------------------------------------------------------------------------------------------------------- |
| Required | Yes |
| Sample Value | `db9...980` |
| Documentation | [Discord Docs](https://discord.com/developers/docs/tutorials/hosting-on-cloudflare-workers#creating-an-app-on-discord) |

### DISCORD_APPLICATION_ID

ID of your application in Discord Developer Portal.

| Question | Answer |
| ------------- | ---------------------------------------------------------------------------------------------------------------------- |
| Required | Yes |
| Sample Value | `427...169` |
| Documentation | [Discord Docs](https://discord.com/developers/docs/tutorials/hosting-on-cloudflare-workers#creating-an-app-on-discord) |

### DISCORD_TOKEN

Bot token of your application in Discord Developer Portal.

| Question | Answer |
| ------------- | ---------------------------------------------------------------------------------------------------------------------- |
| Required | Yes |
| Sample Value | `NDI...LUfg` |
| Documentation | [Discord Docs](https://discord.com/developers/docs/tutorials/hosting-on-cloudflare-workers#creating-an-app-on-discord) |
64 changes: 64 additions & 0 deletions go/discord-command-bot/cli/setup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package main

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"strings"
)

func main() {
err := errorIfEnvMissing([]string{
"DISCORD_PUBLIC_KEY",
"DISCORD_APPLICATION_ID",
"DISCORD_TOKEN",
})
if err != nil {
panic(err)
}

registerApi := "https://discord.com/api/v9/applications/" + os.Getenv("DISCORD_APPLICATION_ID") + "/commands"

bodyJson := map[string]string{"name": "hello", "description": "Hello World Command"}
bodyString, err := json.Marshal(bodyJson)
if err != nil {
panic(err)
}

req, err := http.NewRequest("POST", registerApi, bytes.NewBuffer(bodyString))
if err != nil {
panic(err)
}

req.Header.Set("Authorization", "Bot "+os.Getenv("DISCORD_TOKEN"))
req.Header.Set("Content-Type", "application/json")

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
panic(err)
}

defer resp.Body.Close()

fmt.Println("Command registered successfully")
}

func errorIfEnvMissing(keys []string) error {
missing := []string{}

for _, key := range keys {
if os.Getenv(key) == "" {
missing = append(missing, key)
}
}

if len(missing) > 0 {
return errors.New("Missing required fields: " + strings.Join(missing, ", "))
}

return nil
}
76 changes: 76 additions & 0 deletions go/discord-command-bot/discord.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package handler

import (
"bytes"
"crypto/ed25519"
"encoding/hex"
"encoding/json"
"errors"
"io"
"strings"

"github.com/open-runtimes/types-for-go/v4"
)

type DiscordBodyData struct {
Name string `json:"name"`
}

type DiscordBody struct {
Type int `json:"type"`
Data DiscordBodyData `json:"data"`
}

func discordParseBody(Context *types.Context) (DiscordBody, error) {
var body DiscordBody

err := json.Unmarshal(Context.Req.BodyBinary(), &body)
if err != nil {
return DiscordBody{}, err
}

return body, nil
}

func discordVerifyKey(body string, signature string, timestamp string, discordPublicKey string) error {
var msg bytes.Buffer

if signature == "" || timestamp == "" || discordPublicKey == "" {
return errors.New("payload or headers missing")
}

bytesKey, err := hex.DecodeString(discordPublicKey)
if err != nil {
return err
}

shaKey := ed25519.PublicKey(bytesKey)

bytesSignature, err := hex.DecodeString(signature)
if err != nil {
return err
}

if len(bytesSignature) != ed25519.SignatureSize || bytesSignature[63]&224 != 0 {
return errors.New("invalid signature key")
}

msg.WriteString(timestamp)

bodyReader := strings.NewReader(body)

var bodyBoffer bytes.Buffer

_, err = io.Copy(&msg, io.TeeReader(bodyReader, &bodyBoffer))
if err != nil {
return err
}

success := ed25519.Verify(shaKey, msg.Bytes(), bytesSignature)

if !success {
return errors.New("invalid body")
}

return nil
}
5 changes: 5 additions & 0 deletions go/discord-command-bot/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module openruntimes/handler

go 1.22.5

require github.com/open-runtimes/types-for-go/v4 v4.0.1
2 changes: 2 additions & 0 deletions go/discord-command-bot/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/open-runtimes/types-for-go/v4 v4.0.1 h1:DRPNvUJl3yiiDFUxfs3AqToE78PTmr6KZxJdeCVZbdo=
github.com/open-runtimes/types-for-go/v4 v4.0.1/go.mod h1:88UUMYovXGRbv5keL4uTKDYMWeNtIKV0BbxDRQ18/xY=
65 changes: 65 additions & 0 deletions go/discord-command-bot/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package handler

import (
"os"

"github.com/open-runtimes/types-for-go/v4"
)

type any = map[string]interface{}

func Main(Context *types.Context) types.ResponseOutput {
err := errorIfEnvMissing([]string{
"DISCORD_PUBLIC_KEY",
"DISCORD_APPLICATION_ID",
"DISCORD_TOKEN",
})
if err != nil {
Context.Error(err.Error())
return Context.Res.Text("", 500, nil)
}

err = discordVerifyKey(
Context.Req.BodyText(),
Context.Req.Headers["x-signature-ed25519"],
Context.Req.Headers["x-signature-timestamp"],
os.Getenv("DISCORD_PUBLIC_KEY"),
)
if err != nil {
Context.Error(err.Error())
return Context.Res.Json(any{
"error": "Invalid request signature.",
}, 401, nil)
}

Context.Log("Valid request")

discordBody, err := discordParseBody(Context)
if err != nil {
Context.Error(err.Error())
return Context.Res.Json(any{
"error": "Invalid body.",
}, 400, nil)
}

ApplicationCommandType := 2
if discordBody.Type == ApplicationCommandType && discordBody.Data.Name == "hello" {
Context.Log("Matched hello command - returning message")

channelMessageWithSource := 4
return Context.Res.Json(
any{
"type": channelMessageWithSource,
"data": any{
"content": "Hello, World!",
},
},
200,
nil,
)
}

Context.Log("Didn't match command - returning PONG")

return Context.Res.Json(any{"type": 1}, 200, nil)
}
23 changes: 23 additions & 0 deletions go/discord-command-bot/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package handler

import (
"errors"
"os"
"strings"
)

func errorIfEnvMissing(keys []string) error {
missing := []string{}

for _, key := range keys {
if os.Getenv(key) == "" {
missing = append(missing, key)
}
}

if len(missing) > 0 {
return errors.New("Missing required fields: " + strings.Join(missing, ", "))
}

return nil
}
6 changes: 0 additions & 6 deletions go/starter/.prettierrc.json

This file was deleted.

2 changes: 1 addition & 1 deletion go/starter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Sample `200` Response:
| Setting | Value |
| ----------------- | ------------- |
| Runtime | Go (1.22) |
| Entrypoint | `src/main.go` |
| Entrypoint | `main.go` |
| Permissions | `any` |
| Timeout (Seconds) | 15 |

Expand Down

0 comments on commit 7779c32

Please sign in to comment.