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
|
local cqueues = require 'cqueues'
local websocket = require'http.websocket'
local request = require'http.request'
local json = require 'dkjson'
local class = require 'r.class'
local BasePylon = require 'pylon'
local Channel = require 'channel'
local consts = require 'discord.consts'
local opcodes = consts.opcodes
local API_BASE = "https://discord.com/api/v10/"
local Discord = class.extend(BasePylon)
function Discord.init(self)
self:_check_fields "token"
self.channel_to_webhook = {}
end
local function identify_payload(token)
local I = consts.intents return json.encode{
op = opcodes.identify, d = {
properties = {os="wilson",browser="wilson",device="wilson"},
intents = I.guilds + I.guild_messages + I.message_content,
token = token } } end
function Discord._req(self, url, payload)
::again::
local req = request.new_from_uri(API_BASE..url)
req.headers:upsert(':method',payload and 'POST' or 'GET')
req.headers:upsert('authorization','Bot '..self.token)
req.headers:upsert('user-agent','wilson (https://g.gh0.pw/wilson/, v0.0)')
if payload then
req.headers:upsert('content-type','application/json')
req:set_body(assert(json.encode(payload))) end
local head,body = assert(req:go())
local status = head:get':status'
local bod = body:get_body_as_string()
assert(status:match"^2" or status=="429", 'status was '..status..' body '..bod)
local rl_rem = tonumber(head:get'x-ratelimit-remaining')
local rl_wait = tonumber(head:get'x-ratelimit-reset-after')
self.log:debug('ratelimit',rl_rem,rl_wait)
if status == "429" then
self.log:warn("getting 429'd, waiting",rl_wait)
cqueues.sleep(rl_wait)
goto again
elseif rl_rem < 5 then
local wait = rl_wait -- or rl_wait/(rl_rem+1)? their algorithm is mysterious
self.log:info("ratelimit limit getting low ",rl_rem,", waiting",wait)
if wait>5 then self.log:warn("isn't",wait,"a little long to be waiting?") end
cqueues.sleep(wait)
end
if #bod == 0 then return nil end
local val, _, err = json.decode(bod)
if err then error(err) else return val end
end
function Discord._connect(self)
local uri = "wss://gateway.discord.gg/?v=10&encoding=json"
self.ws = websocket.new_from_uri(uri)
assert(self.ws:connect())
self.ws:send(identify_payload(self.token), 'text')
local me = self:_req"users/@me"
self.bot_id = me.id
self.log('logged in as ',me.username)
for busname, bus in pairs(self.wilson.busses) do
for _, channel in ipairs(bus) do
if channel.pylon == self then
self:add_channel(channel)
end
end
end
end
function Discord.add_channel(self, channel)
local webhooks = self:_req("channels/"..channel.descriptor.."/webhooks")
for _,wh in pairs(webhooks) do
if wh.application_id == self.bot_id then
self.log("Found existing webhook for channel",channel.descriptor)
self.channel_to_webhook[channel.descriptor] = wh
return
end
end
self.log("Making webhook for channel",channel.descriptor)
local wh = self:_req("channels/"..channel.descriptor.."/webhooks", {
name=self.name:gsub("discord",""):gsub("clyde","").." wilson hook" })
self.channel_to_webhook[channel.descriptor] = wh
end
function Discord._heartbeating(self, interval_ms)
local interval = interval_ms / 1000
cqueues.sleep(interval * math.random())
while true do
self.ws:send(json.encode{
op = opcodes.heartbeat,
d = self.sequence_number,
})
cqueues.sleep(interval)
end
end
function Discord.recving(self)
for packet in self.ws:each() do
local event = json.decode(packet)
self.log:debug('event',event.s, event.op, event.t)
if false and event.op ~= opcodes.dispatch then
self.log:debug(event.d)
end
if event.s then self.sequence_number = event.s end
if event.op == opcodes.hello then
self.cq:wrap(self._heartbeating, self, event.d.heartbeat_interval)
elseif event.op == opcodes.dispatch then
self:handle_dispatch(event)
end
end
end
function Discord.handle_dispatch(self, event)
local d = event.d
if event.t == 'MESSAGE_CREATE' then
self.log:proto('<',d.channel_id,d.author.username,d.content)
if
self.channel_to_webhook[d.channel_id]
and d.author.id ~= self.channel_to_webhook[d.channel_id].id
and d.author.id ~= self.bot_id
then
self.wilson:deliver(Channel(self, d.channel_id), {
body = d.content,
sender = '[d]' .. d.author.username,
})
end
end
end
function Discord.sending(self)
for dest_channel, message in self.inbox:iter() do
self.log:proto('>',dest_channel,message.sender,message.body)
local wh = self.channel_to_webhook[dest_channel.descriptor]
self:_req('webhooks/'..wh.id..'/'..wh.token, {
username = message.sender,
content = message.body,
})
end
end
return Discord
|