Skip to content

Commit 7779c32

Browse files
authored
Merge pull request #296 from appwrite/feat-go-discord-command-bot
Feat: Go discord command bot
2 parents 6d251a9 + e342846 commit 7779c32

10 files changed

Lines changed: 319 additions & 7 deletions

File tree

go/discord-command-bot/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Directory used by Appwrite CLI for local development
2+
.appwrite

go/discord-command-bot/README.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# 🤖 Go Discord Command Bot Function
2+
3+
Simple command using Discord Interactions.
4+
5+
## 🧰 Usage
6+
7+
### POST /interactions
8+
9+
Webhook to receive Discord command events. To receive events, you must register your application as a [Discord bot](https://discord.com/developers/applications).
10+
11+
**Parameters**
12+
13+
| Name | Description | Location | Type | Sample Value |
14+
| --------------------- | -------------------------------- | -------- | ------ | --------------------------------------------------------------------------------------------- |
15+
| x-signature-ed25519 | Signature of the request payload | Header | string | `d1efb...aec35` |
16+
| x-signature-timestamp | Timestamp of the request payload | Header | string | `1629837700` |
17+
| JSON Body | GitHub webhook payload | Body | Object | See [Discord docs](https://discord.com/developers/docs/interactions/receiving-and-responding) |
18+
19+
**Response**
20+
21+
Sample `200` Response:
22+
23+
Returns a Discord message object.
24+
25+
```json
26+
{
27+
"type": 4,
28+
"data": {
29+
"content": "Hello from Appwrite 👋"
30+
}
31+
}
32+
```
33+
34+
Sample `401` Response:
35+
36+
```json
37+
{
38+
"error": "Invalid request signature"
39+
}
40+
```
41+
42+
## ⚙️ Configuration
43+
44+
| Setting | Value |
45+
| ----------------- | ------------- |
46+
| Runtime | Go (1.22) |
47+
| Entrypoint | `main.go` |
48+
| Permissions | `any` |
49+
| Timeout (Seconds) | 15 |
50+
51+
## 🔒 Environment Variables
52+
53+
### DISCORD_PUBLIC_KEY
54+
55+
Public Key of your application in Discord Developer Portal.
56+
57+
| Question | Answer |
58+
| ------------- | ---------------------------------------------------------------------------------------------------------------------- |
59+
| Required | Yes |
60+
| Sample Value | `db9...980` |
61+
| Documentation | [Discord Docs](https://discord.com/developers/docs/tutorials/hosting-on-cloudflare-workers#creating-an-app-on-discord) |
62+
63+
### DISCORD_APPLICATION_ID
64+
65+
ID of your application in Discord Developer Portal.
66+
67+
| Question | Answer |
68+
| ------------- | ---------------------------------------------------------------------------------------------------------------------- |
69+
| Required | Yes |
70+
| Sample Value | `427...169` |
71+
| Documentation | [Discord Docs](https://discord.com/developers/docs/tutorials/hosting-on-cloudflare-workers#creating-an-app-on-discord) |
72+
73+
### DISCORD_TOKEN
74+
75+
Bot token of your application in Discord Developer Portal.
76+
77+
| Question | Answer |
78+
| ------------- | ---------------------------------------------------------------------------------------------------------------------- |
79+
| Required | Yes |
80+
| Sample Value | `NDI...LUfg` |
81+
| Documentation | [Discord Docs](https://discord.com/developers/docs/tutorials/hosting-on-cloudflare-workers#creating-an-app-on-discord) |
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"net/http"
9+
"os"
10+
"strings"
11+
)
12+
13+
func main() {
14+
err := errorIfEnvMissing([]string{
15+
"DISCORD_PUBLIC_KEY",
16+
"DISCORD_APPLICATION_ID",
17+
"DISCORD_TOKEN",
18+
})
19+
if err != nil {
20+
panic(err)
21+
}
22+
23+
registerApi := "https://discord.com/api/v9/applications/" + os.Getenv("DISCORD_APPLICATION_ID") + "/commands"
24+
25+
bodyJson := map[string]string{"name": "hello", "description": "Hello World Command"}
26+
bodyString, err := json.Marshal(bodyJson)
27+
if err != nil {
28+
panic(err)
29+
}
30+
31+
req, err := http.NewRequest("POST", registerApi, bytes.NewBuffer(bodyString))
32+
if err != nil {
33+
panic(err)
34+
}
35+
36+
req.Header.Set("Authorization", "Bot "+os.Getenv("DISCORD_TOKEN"))
37+
req.Header.Set("Content-Type", "application/json")
38+
39+
client := &http.Client{}
40+
resp, err := client.Do(req)
41+
if err != nil {
42+
panic(err)
43+
}
44+
45+
defer resp.Body.Close()
46+
47+
fmt.Println("Command registered successfully")
48+
}
49+
50+
func errorIfEnvMissing(keys []string) error {
51+
missing := []string{}
52+
53+
for _, key := range keys {
54+
if os.Getenv(key) == "" {
55+
missing = append(missing, key)
56+
}
57+
}
58+
59+
if len(missing) > 0 {
60+
return errors.New("Missing required fields: " + strings.Join(missing, ", "))
61+
}
62+
63+
return nil
64+
}

go/discord-command-bot/discord.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package handler
2+
3+
import (
4+
"bytes"
5+
"crypto/ed25519"
6+
"encoding/hex"
7+
"encoding/json"
8+
"errors"
9+
"io"
10+
"strings"
11+
12+
"github.com/open-runtimes/types-for-go/v4"
13+
)
14+
15+
type DiscordBodyData struct {
16+
Name string `json:"name"`
17+
}
18+
19+
type DiscordBody struct {
20+
Type int `json:"type"`
21+
Data DiscordBodyData `json:"data"`
22+
}
23+
24+
func discordParseBody(Context *types.Context) (DiscordBody, error) {
25+
var body DiscordBody
26+
27+
err := json.Unmarshal(Context.Req.BodyBinary(), &body)
28+
if err != nil {
29+
return DiscordBody{}, err
30+
}
31+
32+
return body, nil
33+
}
34+
35+
func discordVerifyKey(body string, signature string, timestamp string, discordPublicKey string) error {
36+
var msg bytes.Buffer
37+
38+
if signature == "" || timestamp == "" || discordPublicKey == "" {
39+
return errors.New("payload or headers missing")
40+
}
41+
42+
bytesKey, err := hex.DecodeString(discordPublicKey)
43+
if err != nil {
44+
return err
45+
}
46+
47+
shaKey := ed25519.PublicKey(bytesKey)
48+
49+
bytesSignature, err := hex.DecodeString(signature)
50+
if err != nil {
51+
return err
52+
}
53+
54+
if len(bytesSignature) != ed25519.SignatureSize || bytesSignature[63]&224 != 0 {
55+
return errors.New("invalid signature key")
56+
}
57+
58+
msg.WriteString(timestamp)
59+
60+
bodyReader := strings.NewReader(body)
61+
62+
var bodyBoffer bytes.Buffer
63+
64+
_, err = io.Copy(&msg, io.TeeReader(bodyReader, &bodyBoffer))
65+
if err != nil {
66+
return err
67+
}
68+
69+
success := ed25519.Verify(shaKey, msg.Bytes(), bytesSignature)
70+
71+
if !success {
72+
return errors.New("invalid body")
73+
}
74+
75+
return nil
76+
}

go/discord-command-bot/go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module openruntimes/handler
2+
3+
go 1.22.5
4+
5+
require github.com/open-runtimes/types-for-go/v4 v4.0.1

go/discord-command-bot/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
github.com/open-runtimes/types-for-go/v4 v4.0.1 h1:DRPNvUJl3yiiDFUxfs3AqToE78PTmr6KZxJdeCVZbdo=
2+
github.com/open-runtimes/types-for-go/v4 v4.0.1/go.mod h1:88UUMYovXGRbv5keL4uTKDYMWeNtIKV0BbxDRQ18/xY=

go/discord-command-bot/main.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package handler
2+
3+
import (
4+
"os"
5+
6+
"github.com/open-runtimes/types-for-go/v4"
7+
)
8+
9+
type any = map[string]interface{}
10+
11+
func Main(Context *types.Context) types.ResponseOutput {
12+
err := errorIfEnvMissing([]string{
13+
"DISCORD_PUBLIC_KEY",
14+
"DISCORD_APPLICATION_ID",
15+
"DISCORD_TOKEN",
16+
})
17+
if err != nil {
18+
Context.Error(err.Error())
19+
return Context.Res.Text("", 500, nil)
20+
}
21+
22+
err = discordVerifyKey(
23+
Context.Req.BodyText(),
24+
Context.Req.Headers["x-signature-ed25519"],
25+
Context.Req.Headers["x-signature-timestamp"],
26+
os.Getenv("DISCORD_PUBLIC_KEY"),
27+
)
28+
if err != nil {
29+
Context.Error(err.Error())
30+
return Context.Res.Json(any{
31+
"error": "Invalid request signature.",
32+
}, 401, nil)
33+
}
34+
35+
Context.Log("Valid request")
36+
37+
discordBody, err := discordParseBody(Context)
38+
if err != nil {
39+
Context.Error(err.Error())
40+
return Context.Res.Json(any{
41+
"error": "Invalid body.",
42+
}, 400, nil)
43+
}
44+
45+
ApplicationCommandType := 2
46+
if discordBody.Type == ApplicationCommandType && discordBody.Data.Name == "hello" {
47+
Context.Log("Matched hello command - returning message")
48+
49+
channelMessageWithSource := 4
50+
return Context.Res.Json(
51+
any{
52+
"type": channelMessageWithSource,
53+
"data": any{
54+
"content": "Hello, World!",
55+
},
56+
},
57+
200,
58+
nil,
59+
)
60+
}
61+
62+
Context.Log("Didn't match command - returning PONG")
63+
64+
return Context.Res.Json(any{"type": 1}, 200, nil)
65+
}

go/discord-command-bot/utils.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package handler
2+
3+
import (
4+
"errors"
5+
"os"
6+
"strings"
7+
)
8+
9+
func errorIfEnvMissing(keys []string) error {
10+
missing := []string{}
11+
12+
for _, key := range keys {
13+
if os.Getenv(key) == "" {
14+
missing = append(missing, key)
15+
}
16+
}
17+
18+
if len(missing) > 0 {
19+
return errors.New("Missing required fields: " + strings.Join(missing, ", "))
20+
}
21+
22+
return nil
23+
}

go/starter/.prettierrc.json

Lines changed: 0 additions & 6 deletions
This file was deleted.

go/starter/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Sample `200` Response:
3838
| Setting | Value |
3939
| ----------------- | ------------- |
4040
| Runtime | Go (1.22) |
41-
| Entrypoint | `src/main.go` |
41+
| Entrypoint | `main.go` |
4242
| Permissions | `any` |
4343
| Timeout (Seconds) | 15 |
4444

0 commit comments

Comments
 (0)