summaryrefslogtreecommitdiff
path: root/discord/pylon.lua
blob: 15a1418dc9cf9da9b06598a10af891119521ca8e (plain)
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 <= 3 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