-
Notifications
You must be signed in to change notification settings - Fork 105
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #296 from appwrite/feat-go-discord-command-bot
Feat: Go discord command bot
- Loading branch information
Showing
10 changed files
with
319 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
# Directory used by Appwrite CLI for local development | ||
.appwrite |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters