From 1414dff029b519757eb54f5b37f76b1349cd28f4 Mon Sep 17 00:00:00 2001 From: null8626 Date: Wed, 17 Sep 2025 00:19:10 +0700 Subject: [PATCH 1/3] feat: add webhooks --- topgg/init.lua | 14 ++-- topgg/lib/webhooks.lua | 176 +++++++++++++++++++++++++++++++++++++++++ topgg/package.lua | 39 ++++----- 3 files changed, 205 insertions(+), 24 deletions(-) create mode 100644 topgg/lib/webhooks.lua diff --git a/topgg/init.lua b/topgg/init.lua index 220bc04..04bbbc3 100644 --- a/topgg/init.lua +++ b/topgg/init.lua @@ -1,6 +1,10 @@ -package.path = './deps/?/init.lua;./deps/?.lua;./topgg/lib/?.lua;./deps/secure-socket/?.lua;' .. package.path; +package.path = + './deps/?/init.lua;./deps/?.lua;./topgg/lib/?.lua;./deps/secure-socket/?.lua;' .. package.path + return { - Api = require('api'), - Autoposter = require('autoposter'), - test = require('test') -} \ No newline at end of file + Api = require('api'), + Webhooks = require('webhooks'), + test = function() + print('[topgg-lua TEST] Library loaded successfully') + end, +} diff --git a/topgg/lib/webhooks.lua b/topgg/lib/webhooks.lua new file mode 100644 index 0000000..4e1099b --- /dev/null +++ b/topgg/lib/webhooks.lua @@ -0,0 +1,176 @@ +local http = require('coro-http') +local json = require('json') +local tls = require('tls') + +local Webhooks = {} + +Webhooks.__index = Webhooks + +function Webhooks:new_with_authorization(authorization) + local webhooks = setmetatable({}, self) + + webhooks.authorization = authorization + webhooks.routes = {} + + return webhooks +end + +function Webhooks:new(authorization) + local webhooks = setmetatable({}, self) + + webhooks.authorization = authorization + webhooks.routes = {} + + return webhooks +end + +function Webhooks:add(path, callback, authorization) + authorization = authorization or self.authorization + + if type(callback) ~= 'function' then + error("argument 'callback' must be a function") + elseif type(authorization) ~= 'string' then + error('route must have a clear authorization') + end + + table.insert(self.routes, { + path = path, + authorization = authorization, + callback = callback, + }) +end + +function Webhooks:start(input) + if not input or type(input.host) ~= 'string' or type( + input.port + ) ~= 'number' then + error('missing host and port') + end + + if input.key and input.cert then + tls.createServer( + { + key = input.key, + cert = input.cert, + }, + function(socket) + local body = '' + + socket:on('data', function(chunk) + body = body .. chunk + + if body:find('\r\n\r\n') then + local method = body:match('^(%S+)') + + if method == 'POST' then + local url = body:match('^%S+%s+(%S+)') + local headers = + (body:match('^(.-)\r\n\r\n') or ''):gmatch('[^\r\n]+') + + for _, route in pairs(self.routes) do + if string.sub(url, 1, string.len(route.path)) == route.path then + for line in headers do + local key, value = line:match('^([^:]+):%s*(.*)$') + + if key and string.lower(key) == 'authorization' then + if value ~= route.authorization then + socket:write( + 'HTTP/1.1 401 Unauthorized\r\n\r\nUnauthorized' + ) + socket:destroy() + return + end + + local payload = body:match('\r\n\r\n(.*)') + local json_body = json.decode(payload or '') + + if json_body then + route.callback(json_body) + + socket:write('HTTP/1.1 204 No Content\r\n\r\n') + else + socket:write( + 'HTTP/1.1 400 Bad Request\r\n\r\nInvalid JSON body' + ) + end + + socket:destroy() + return + end + end + + socket:write('HTTP/1.1 401 Unauthorized\r\n\r\nUnauthorized') + socket:destroy() + return + end + end + end + + socket:write('HTTP/1.1 404 Not Found\r\n\r\nNot Found') + socket:destroy() + end + end) + end + ):listen(input.port, input.host) + else + http.createServer(input.host, input.port, function(request, body) + if request.method == 'POST' then + for _, route in pairs(self.routes) do + if string.sub( + request.path, + 1, + string.len(route.path) + ) == route.path then + for _, header in ipairs(request) do + if type(header[1]) == 'string' and string.lower( + header[1] + ) == 'authorization' then + if header[2] == route.authorization then + local json_body, err = json.decode(body) + + if json_body then + route.callback(json_body) + + return { + { 'Content-Type', 'text/html' }, + { 'Content-Length', '0' }, + code = 204, + reason = 'No Content', + version = 1.1, + }, '' + else + return { + { 'Content-Type', 'text/html' }, + { 'Content-Length', '17' }, + code = 400, + reason = 'Bad Request', + version = 1.1, + }, 'Invalid JSON body' + end + end + end + end + + return { + { 'Content-Type', 'text/html' }, + { 'Content-Length', '12' }, + code = 401, + reason = 'Unauthorized', + version = 1.1, + }, 'Unauthorized' + end + end + end + + return { + { 'Content-Type', 'text/html' }, + { 'Content-Length', '9' }, + code = 404, + reason = 'Not Found', + version = 1.1, + }, 'Not Found' + end) + end +end + +return Webhooks diff --git a/topgg/package.lua b/topgg/package.lua index 40914b4..651de18 100644 --- a/topgg/package.lua +++ b/topgg/package.lua @@ -1,19 +1,20 @@ - return { - name = "topgg-lua", - version = "0.0.1", - description = "A library for top.gg, in lua", - tags = { "dbl", "topgg", "top.gg" }, - license = "MIT", - author = { name = "matthewthechickenman", email = "65732060+matthewthechickenman@users.noreply.github.com" }, - homepage = "https://github.com/matthewthechickenman/topgg-lua", - dependencies = { - "creationix/coro-http", - "luvit/json", - "luvit/secure-socket" - }, - files = { - "**.lua", - "!test*" - } - } - \ No newline at end of file +return { + name = 'topgg-lua', + version = '1.0.0', + description = 'A library for top.gg, in lua', + tags = { 'dbl', 'topgg', 'top.gg' }, + license = 'MIT', + author = { + name = 'matthew-st', + email = '65732060+matthewthechickenman@users.noreply.github.com', + }, + homepage = 'https://github.com/Top-gg-Community/lua-sdk', + dependencies = { + 'creationix/coro-http', + 'luvit/json', + 'luvit/secure-socket', + 'luvit/timer', + 'luvit/tls', + }, + files = { '**.lua', '!test*' }, +} From 65b52062839835d57b64aa8ccf059e319f43b570 Mon Sep 17 00:00:00 2001 From: null8626 Date: Fri, 19 Sep 2025 14:25:48 +0700 Subject: [PATCH 2/3] refactor: use http-codec instead of directly processing raw HTTP body --- topgg/lib/webhooks.lua | 202 ++++++++++++++++++++--------------------- topgg/package.lua | 1 + 2 files changed, 98 insertions(+), 105 deletions(-) diff --git a/topgg/lib/webhooks.lua b/topgg/lib/webhooks.lua index 4e1099b..a9a481d 100644 --- a/topgg/lib/webhooks.lua +++ b/topgg/lib/webhooks.lua @@ -1,12 +1,52 @@ +local codec = require('http-codec') local http = require('coro-http') local json = require('json') local tls = require('tls') +local ResponseType = { + SUCCESS = 204, + INVALID_REQUEST = 400, + UNAUTHORIZED = 401, + NOT_FOUND = 404, +} + +local function getHeaderValue(request, input) + if request.headers then + for key, value in pairs(request.headers) do + if type(key) == 'string' and string.lower(key) == input then + return value + end + end + else + for _, header in ipairs(request) do + local key = header[1] + + if type(key) == 'string' and string.lower(key) == input then + return header[2] + end + end + end + + return nil +end + +local function responseTypeToString(response) + if response == ResponseType.SUCCESS then + return 'No Content' + elseif response == ResponseType.INVALID_REQUEST then + return 'Bad Request' + elseif response == ResponseType.UNAUTHORIZED then + return 'Unauthorized' + elseif response == ResponseType.NOT_FOUND then + return 'Not Found' + end +end + local Webhooks = {} Webhooks.__index = Webhooks -function Webhooks:new_with_authorization(authorization) +function Webhooks:newWithAuthorization(authorization) local webhooks = setmetatable({}, self) webhooks.authorization = authorization @@ -28,7 +68,7 @@ function Webhooks:add(path, callback, authorization) authorization = authorization or self.authorization if type(callback) ~= 'function' then - error("argument 'callback' must be a function") + error('argument \'callback\' must be a function') elseif type(authorization) ~= 'string' then error('route must have a clear authorization') end @@ -40,6 +80,36 @@ function Webhooks:add(path, callback, authorization) }) end +function Webhooks:resolve(request, body) + if request.method == 'POST' then + for _, route in pairs(self.routes) do + if string.sub( + request.path, + 1, + string.len(route.path) + ) == route.path then + local incomingAuthorization = getHeaderValue(request, 'authorization') + + if incomingAuthorization and incomingAuthorization == route.authorization then + local json_body, err = json.decode(body) + + if json_body then + route.callback(json_body) + + return ResponseType.SUCCESS + else + return ResponseType.INVALID_REQUEST + end + end + + return ResponseType.UNAUTHORIZED + end + end + end + + return ResponseType.NOT_FOUND +end + function Webhooks:start(input) if not input or type(input.host) ~= 'string' or type( input.port @@ -53,122 +123,44 @@ function Webhooks:start(input) key = input.key, cert = input.cert, }, - function(socket) - local body = '' + function (socket) + local decode = codec.decoder() + local data = '' socket:on('data', function(chunk) - body = body .. chunk - - if body:find('\r\n\r\n') then - local method = body:match('^(%S+)') - - if method == 'POST' then - local url = body:match('^%S+%s+(%S+)') - local headers = - (body:match('^(.-)\r\n\r\n') or ''):gmatch('[^\r\n]+') - - for _, route in pairs(self.routes) do - if string.sub(url, 1, string.len(route.path)) == route.path then - for line in headers do - local key, value = line:match('^([^:]+):%s*(.*)$') - - if key and string.lower(key) == 'authorization' then - if value ~= route.authorization then - socket:write( - 'HTTP/1.1 401 Unauthorized\r\n\r\nUnauthorized' - ) - socket:destroy() - return - end - - local payload = body:match('\r\n\r\n(.*)') - local json_body = json.decode(payload or '') - - if json_body then - route.callback(json_body) - - socket:write('HTTP/1.1 204 No Content\r\n\r\n') - else - socket:write( - 'HTTP/1.1 400 Bad Request\r\n\r\nInvalid JSON body' - ) - end - - socket:destroy() - return - end - end - - socket:write('HTTP/1.1 401 Unauthorized\r\n\r\nUnauthorized') - socket:destroy() - return - end + data = data .. chunk + + if data:find('\r\n\r\n') then + local request = decode(data, 1) + + if request then + local body = data:match('\r\n\r\n(.*)') + local contentLength = getHeaderValue(request, 'content-length') + + if contentLength and tonumber(contentLength) == #body then + local response = self:resolve(request, body) + local reason = responseTypeToString(response) + + socket:write(string.format('HTTP/1.1 %d %s\r\n\r\n%s', response, reason, reason)) + socket:destroy() end end - - socket:write('HTTP/1.1 404 Not Found\r\n\r\nNot Found') - socket:destroy() end end) end ):listen(input.port, input.host) else - http.createServer(input.host, input.port, function(request, body) - if request.method == 'POST' then - for _, route in pairs(self.routes) do - if string.sub( - request.path, - 1, - string.len(route.path) - ) == route.path then - for _, header in ipairs(request) do - if type(header[1]) == 'string' and string.lower( - header[1] - ) == 'authorization' then - if header[2] == route.authorization then - local json_body, err = json.decode(body) - - if json_body then - route.callback(json_body) - - return { - { 'Content-Type', 'text/html' }, - { 'Content-Length', '0' }, - code = 204, - reason = 'No Content', - version = 1.1, - }, '' - else - return { - { 'Content-Type', 'text/html' }, - { 'Content-Length', '17' }, - code = 400, - reason = 'Bad Request', - version = 1.1, - }, 'Invalid JSON body' - end - end - end - end - - return { - { 'Content-Type', 'text/html' }, - { 'Content-Length', '12' }, - code = 401, - reason = 'Unauthorized', - version = 1.1, - }, 'Unauthorized' - end - end - end + http.createServer(input.host, input.port, function (request, body) + local response = self:resolve(request, body) + local reason = responseTypeToString(response) return { { 'Content-Type', 'text/html' }, - { 'Content-Length', '9' }, - code = 404, - reason = 'Not Found', + { 'Content-Length', tostring(#reason) }, + code = response, + reason = reason, version = 1.1, - }, 'Not Found' + }, reason end) end end diff --git a/topgg/package.lua b/topgg/package.lua index 651de18..3497e10 100644 --- a/topgg/package.lua +++ b/topgg/package.lua @@ -15,6 +15,7 @@ return { 'luvit/secure-socket', 'luvit/timer', 'luvit/tls', + 'luvit/http-codec', }, files = { '**.lua', '!test*' }, } From 28a6ea35a7339b41fe6944af2323ae7b31b0edfa Mon Sep 17 00:00:00 2001 From: null8626 Date: Tue, 30 Sep 2025 22:35:53 +0700 Subject: [PATCH 3/3] meta: make version 0.1.0 instead of 1.0.0 --- topgg/package.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/topgg/package.lua b/topgg/package.lua index 3497e10..cbd102b 100644 --- a/topgg/package.lua +++ b/topgg/package.lua @@ -1,6 +1,6 @@ return { name = 'topgg-lua', - version = '1.0.0', + version = '0.1.0', description = 'A library for top.gg, in lua', tags = { 'dbl', 'topgg', 'top.gg' }, license = 'MIT',