-
Notifications
You must be signed in to change notification settings - Fork 1
/
discord-bot.pluto
306 lines (277 loc) · 8.01 KB
/
discord-bot.pluto
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
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
local http = require "pluto:http"
local json = require "pluto:json"
local pluto_scheduler = require "pluto:scheduler"
local socket = require "pluto:socket"
require "websocket"
local function new_with_data(bot, clazz, data)
local inst = new clazz()
inst.client = bot
for k, v in data do
inst[k] = v
end
return inst
end
local DiscordMessage
local class DiscordChannelId
__name = "DiscordChannelId"
function __construct(public client, public id)
end
function sendTyping()
self.client:sendRequest("POST", $"/channels/{self.id}/typing")
end
function sendMessage(content)
local data = self.client:sendRequest("POST", $"/channels/{self.id}/messages", {
["content"] = content
})
local msg = new_with_data(self.client, DiscordMessage, data)
msg.channel = self
if data.guild_id then
msg.guild = new DiscordGuildId(self.client, data.guild_id)
end
return msg
end
end
local class DiscordGuildId
__name = "DiscordGuildId"
function __construct(public client, public id)
end
function addBan(user_id, reason = "", delete_message_days = 0)
self.client:sendRequest("PUT", $"/guilds/{self.id}/bans/{user_id}", {
["__audit_log_reason"] = reason,
["delete_message_days"] = delete_message_days
})
end
function leave()
self.client:sendRequest("DELETE", $"/users/@me/guilds/{self.id}")
end
end
class DiscordMessage
__name = "DiscordMessage"
function mentionsMe()
for self.mentions as mention do
if mention.id == self.client.user.id then
return true
end
end
return false
end
function reply(content)
local data = self.client:sendRequest("POST", $"/channels/{self.channel.id}/messages", {
["content"] = content,
["message_reference"] = {
["message_id"] = self.id,
["fail_if_not_exists"] = false,
}
})
local msg = new_with_data(self.client, DiscordMessage, data)
msg.channel = self.channel
msg.guild = self.guild
return msg
end
function edit(content)
self.client:sendRequest("PATCH", $"/channels/{self.channel.id}/messages/{self.id}", {
["content"] = content
})
end
function delete()
self.client:sendRequest("DELETE", $"/channels/{self.channel.id}/messages/{self.id}")
end
end
local class Heartbeater
last_sent = 0
function start(scheduler, ws, interval)
self.ws = ws
scheduler:addloop(function()
if self.ws ~= ws then
return false
end
if self.last_seq and os.millis() - self.last_sent > interval then
--print("sending heartbeat")
self.ws:wsSend(json.encode({
op = 1,
d = self.last_seq
}))
self.last_sent = os.millis()
end
end)
end
function stop()
self.ws = nil
end
end
local GUILDS <const> = (1 << 0)
local GUILD_MEMBERS <const> = (1 << 1)
local GUILD_BANS <const> = (1 << 2)
local GUILD_EMOJIS_AND_STICKERS <const> = (1 << 3)
local GUILD_INTEGRATIONS <const> = (1 << 4)
local GUILD_WEBHOOKS <const> = (1 << 5)
local GUILD_INVITES <const> = (1 << 6)
local GUILD_VOICE_STATES <const> = (1 << 7)
local GUILD_PRESENCES <const> = (1 << 8)
local GUILD_MESSAGES <const> = (1 << 9)
local GUILD_MESSAGE_REACTIONS <const> = (1 << 10)
local GUILD_MESSAGE_TYPING <const> = (1 << 11)
local DIRECT_MESSAGES <const> = (1 << 12)
local DIRECT_MESSAGE_REACTIONS <const> = (1 << 13)
local DIRECT_MESSAGE_TYPING <const> = (1 << 14)
local MESSAGE_CONTENT <const> = (1 << 15)
local GUILD_SCHEDULED_EVENTS <const> = (1 << 16)
return class
__name = "DiscordBot"
private token = nil
user = {}
private on_message = nil
function __construct(token, scheduler)
self.scheduler = scheduler ?? new pluto_scheduler()
self.heartbeater = new Heartbeater()
self.token = token
self.scheduler:add(function()
local ws = self.internal_openConnection()
local data = ws:wsRecv()
print(data)
self.heartbeat_interval = json.decode(data).d.heartbeat_interval
self.heartbeater:start(self.scheduler, ws, self.heartbeat_interval)
ws:wsSend(json.encode({
op = 2,
d = {
token = token,
intents = GUILDS | GUILD_MEMBERS | GUILD_MESSAGES | MESSAGE_CONTENT | DIRECT_MESSAGES,
properties = {
os = "linux",
browser = "pluto-discord-bot",
device = "pluto-discord-bot",
}
}
}))
return self:internal_runEventLoop(ws)
end)
end
static function internal_openConnection()
local ws
repeat
print("Connecting to gateway.discord.gg...")
ws = socket.connect("gateway.discord.gg", 443)
until ws ~= nil
if not ws:starttls("gateway.discord.gg") then
error("TLS handshake failed.")
end
if not ws:wsUpgrade("gateway.discord.gg", "/?v=10&encoding=json") then
error("WebSocket upgrade failed.")
end
print("Connected to gateway.")
return ws
end
function internal_runEventLoop(ws)
while true do
local data
try
data = ws:wsRecv()
catch e then
print(e)
end
if data == nil then
self.heartbeater:stop()
if not self.session_id then
return -- in this case, the error message probably was "Authentication failed. (4004)"
end
print("Broken pipe, attempting to reconnect and resume.")
ws = self.internal_openConnection()
ws:wsSend(json.encode({
op = 6,
d = {
token = self.token,
session_id = self.session_id,
seq = self.heartbeater.last_seq
}
}))
data = ws:wsRecv()
print(data)
self.heartbeater:start(self.scheduler, ws, self.heartbeat_interval)
end
print(data)
data = json.decode(data)
if data.s then
self.heartbeater.last_seq = data.s
end
switch data.op do
case 0: -- Dispatch
switch data.t do
case "READY":
if not self.session_id then -- Auth succeeded for first time?
-- Keep HTTP connection warmed up to avoid latency when we need to fire a request
self.scheduler:addloop(function()
if not http.hasconnection("https://discord.com") then
print("Connecting to https://discord.com...")
http.request("https://discord.com/api/v10/gateway")
end
end)
end
self.user = data.d.user
self.session_id = data.d.session_id
self.resume_gateway_url = data.d.resume_gateway_url
break
case "MESSAGE_CREATE":
if self.on_message then
local msg = new_with_data(self, DiscordMessage, data.d)
msg.channel = new DiscordChannelId(self, data.d.channel_id)
if data.d.guild_id then
msg.guild = new DiscordGuildId(self, data.d.guild_id)
end
self.scheduler:add(function()
self.on_message(msg)
end)
end
break
end
break
case 7: -- Reconnect
print("Remote is asking us to reconnect, dropping connection.")
self.heartbeater:stop()
ws = self.internal_openConnection()
ws:wsSend(json.encode({
op = 6,
d = {
token = self.token,
session_id = self.session_id,
seq = self.heartbeater.last_seq
}
}))
data = ws:wsRecv()
print(data)
self.heartbeater:start(self.scheduler, ws, self.heartbeat_interval)
break
end
end
end
function onMessage(f)
self.on_message = f
end
function run()
self.scheduler:run()
end
function sendRequest(method, endpoint, body)
endpoint = "https://discord.com/api/v10"..endpoint
print($"[HTTP] Sending {method} request to {endpoint}")
local options = {
["method"] = method,
["url"] = endpoint,
["headers"] = {
["Authorization"] = $"Bot {self.token}",
["User-Agent"] = "DiscordBot (Please momma no spaghetti)",
}
}
if body then
options.headers["Content-Type"] = "application/json"
if body.__audit_log_reason then
options.headers["X-Audit-Log-Reason"] = body.__audit_log_reason
body.__audit_log_reason = nil
end
options.body = json.encode(body)
end
local response = http.request(options)
if response ~= "" then
print($"[HTTP] Response: {response}")
end
return json.decode(response)
end
end