-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.go
244 lines (219 loc) · 6.46 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
package main
import (
"context"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"os/signal"
"path/filepath"
"regexp"
"strings"
"github.com/bwmarrin/discordgo"
"github.com/google/go-github/v43/github"
piston "github.com/milindmadhukar/go-piston"
)
var s *discordgo.Session
var gclient *github.Client
var ctx context.Context
// bot parameters
var (
botToken string
pclient *piston.Client
)
// code execution types
const (
cblock int = iota
cgist
cfile
)
// code execution ouptput
var o chan string
func init() {
botToken = os.Getenv("BOT_TOKEN")
flag.Parse()
pclient = piston.CreateDefaultClient()
ctx = context.Background()
gclient = github.NewClient(nil)
o = make(chan string)
}
// create discord session
func init() {
var err error
s, err = discordgo.New("Bot " + botToken)
if err != nil {
log.Fatalf("Invalid bot parameters: %v", err)
}
}
func main() {
// add function handlers for code execution
s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) { log.Println("Bot is up!") })
s.AddHandler(executionHandler)
s.AddHandler(reExecuctionHandler)
err := s.Open()
if err != nil {
log.Fatalf("Cannot open the session: %v", err)
}
defer s.Close()
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt)
<-stop
log.Println("Graceful shutdown")
}
func executionHandler(s *discordgo.Session, m *discordgo.MessageCreate) {
// avoid handling the message that the bot creates when replying to a user
if m.Author.Bot {
return
}
// extract code block from message and execute code
var responseContent string
ctype, lang, codeBlock := codeBlockExtractor(m.Message)
if lang != "" && codeBlock != "" {
go exec(m.ChannelID, codeBlock, m.Reference(), lang)
responseContent = <-o
} else {
return
}
// only add run button for code block and gist execution
var runButton []discordgo.MessageComponent
if ctype != cfile {
runButton = []discordgo.MessageComponent{discordgo.ActionsRow{
Components: []discordgo.MessageComponent{
discordgo.Button{
Label: "Run",
Style: discordgo.SuccessButton,
CustomID: "run",
},
},
},
}
}
// send initial reply message containing output of code execution
// "Run" button is injected in the message so the user may re run their code
_, _ = s.ChannelMessageSendComplex(m.ChannelID, &discordgo.MessageSend{
Content: responseContent,
Reference: m.Reference(),
Components: runButton,
})
}
// handler for re-executing go code when the "Run" button is clicked
func reExecuctionHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
// check if go button was clicked
if i.MessageComponentData().CustomID == "run" {
// get referenced channel message
// used to fetch the code from the message that contains it
m, err := s.ChannelMessage(i.ChannelID, i.Message.MessageReference.MessageID)
if err != nil {
log.Printf("Could not get message reference: %v", err)
}
// extract code block from message and execute code
var responseContent string
if _, lang, codeBlock := codeBlockExtractor(m); lang != "" || codeBlock != "" {
go exec(i.ChannelID, codeBlock, i.Message.Reference(), lang)
responseContent = <-o
} else {
responseContent = fmt.Sprintln("Could not find any code in message to execute")
}
// send interaction respond
// update message reply with new code execution output
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseUpdateMessage,
Data: &discordgo.InteractionResponseData{
Content: responseContent,
Components: []discordgo.MessageComponent{
discordgo.ActionsRow{
Components: []discordgo.MessageComponent{
discordgo.Button{
Label: "Run",
Style: discordgo.SuccessButton,
CustomID: "run",
},
},
},
},
},
})
if err != nil {
log.Printf("Could not send respond interaction: %v", err)
}
}
}
// handle code execution
// sends output to chan
func exec(channelID string, code string, messageReference *discordgo.MessageReference, lang string) {
// execute code using piston library
output, err := pclient.Execute(lang, "",
[]piston.Code{
{
Name: fmt.Sprintf("%s-code", messageReference.MessageID),
Content: code,
},
},
)
if err != nil {
o <- fmt.Sprintf(">>> Execution Failed [%s - %s]\n```\n%s\n```\n", output.Language, output.Version, err)
}
o <- fmt.Sprintf(">>> Output [%s - %s]\n```\n%s\n```\n", output.Language, output.Version, output.GetOutput())
}
func codeBlockExtractor(m *discordgo.Message) (int, string, string) {
mc := m.Content
// syntax for executing a code block
// this is based on a writing standard in discord for writing code in a paragraph message block
// example message: ```go ... ```
rcb, _ := regexp.Compile("run```.*")
// syntax for executing a gist
rg, _ := regexp.Compile("run https://gist.github.com/.*/.*")
rgist, _ := regexp.Compile("run https://gist.github.com/.*/")
// syntax for executing an attached file
rf, _ := regexp.Compile("run *.*")
c := strings.Split(mc, "\n")
for bi, bb := range c {
// extract code block to execute
if rcb.MatchString(bb) {
lang := strings.TrimPrefix(string(rcb.Find([]byte(mc))), "run```")
// find end of code block
var codeBlock string
endBlockRegx, _ := regexp.Compile("```")
sa := c[bi+1:]
for ei, eb := range sa {
if endBlockRegx.Match([]byte(eb)) {
// create code block to execute
codeBlock = strings.Join(sa[:ei], "\n")
return cblock, lang, codeBlock
}
}
}
// extract gist language and code to execute
if rg.MatchString(bb) {
gistID := rgist.ReplaceAllString(bb, "")
gist, _, err := gclient.Gists.Get(ctx, gistID)
if err != nil {
log.Printf("Failed to obtain gist: %v\n", err)
}
return cgist, strings.ToLower(*gist.Files["helloworld.go"].Language), *gist.Files["helloworld.go"].Content
}
// extract file language and code to execute
if rf.MatchString(bb) {
if len(m.Attachments) > 0 {
// handle 1 file in message attachments
f := m.Attachments[0]
// get language from extension
lang := strings.TrimLeft(filepath.Ext(f.Filename), ".")
// get code from file
resp, err := http.Get(f.URL)
if err != nil {
log.Printf("Failed GET http call to file attachment URL: %v\n", err)
}
defer resp.Body.Close()
codeBlock, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Failed to obtain code from response body: %v\n", err)
}
return cfile, lang, string(codeBlock)
}
}
}
return -1, "", ""
}