IMIsle Modkit
Pages in this area

HTTP requests and Discord webhooks

Server scripts can call web services with modkit.http. The request runs on a worker thread, the game keeps running, and your callback gets the answer a moment later. HTTPS works.

The webhook URL belongs into server.cfg

A Discord webhook URL is a secret. Anyone who has it can post into your channel. Keep it out of the script and read it with modkit.config.

set discord_webhook https://discord.com/api/webhooks/123456789/your-token

Use set, not sets. Lines with sets are shown in the public server list.

A complete resource

resources/discordlog/manifest.lua

name "discordlog"
server "server.lua"

resources/discordlog/server.lua

local WEBHOOK = modkit.config.get("discord_webhook")
if not WEBHOOK then
  modkit.log.warn("set discord_webhook in server.cfg to use this resource")
  return
end

local queue = {}
local sending = false

local function pump()
  if sending or #queue == 0 then return end
  sending = true
  local text = table.remove(queue, 1)
  modkit.http.postJson(WEBHOOK, { content = text }, function(res)
    sending = false
    if res.status == 429 then
      local wait = tonumber(res.headers["retry-after"]) or 2
      table.insert(queue, 1, text)
      modkit.setTimeout(math.ceil(wait * 1000), pump)
      return
    end
    if not res.ok then modkit.log.warn("discord answered " .. res.status .. " " .. tostring(res.error)) end
    pump()
  end)
end

local function say(text)
  if #queue >= 50 then table.remove(queue, 1) end
  queue[#queue + 1] = text:sub(1, 1900)
  pump()
end

modkit.events.add("serverStarted", function() say("Server is up.") end)
modkit.events.add("playerConnected", function(player)
  say(player.name .. " joined (" .. modkit.players.count() .. " online)")
end)
modkit.events.add("playerDisconnected", function(player) say(player.name .. " left") end)
modkit.events.add("playerRejected", function(player, reason) say(player.name .. " was rejected: " .. reason) end)
modkit.events.add("playerDied", function(player, killer)
  if killer and killer.steamId then
    say(killer.name .. " (" .. killer.species .. ") killed " .. player.name .. " (" .. player.species .. ")")
  end
end)
modkit.events.add("chat", function(player, text)
  say("**" .. player.name .. "**: " .. text)
end)

The queue sends one message at a time. Discord limits how fast a webhook may post and answers with status 429 and a retry-after header when you are too fast. The script waits that long and tries again. Without a queue a busy chat would lose messages.

text:sub(1, 1900) keeps a message under Discord's limit of 2000 characters.

Reading an API

modkit.http.get("https://api.example.org/motd", function(res)
  if not res.ok then return end
  local ok, data = pcall(modkit.json.decode, res.body)
  if ok and type(data) == "table" and data.text then modkit.world.announce(data.text, "News", 10) end
end)

Always check res.ok, and wrap json.decode in pcall. A web service can answer with an error page instead of JSON.

Rules of thumb

  • The callback runs on the game thread like every other handler. Keep it short.
  • At most 128 requests can wait at once. Four run at the same time. A request that does not fit raises an error in modkit.http.request.
  • A response larger than 4 MB is cut and error is set.
  • status is 0 when the server was not reached. error then holds the step that failed and the Windows error code, for example request failed (12007) for a host name that does not resolve.
  • The default timeout is 15 seconds, set timeout in milliseconds to change it.
  • Client scripts have no HTTP function. Let the client ask the server through an event and answer with player:call.