How would I add support for the Newgrounds API in my Defold game? Since there’s no library available, I’d have to adapt the requests to the Defold engine.
Are you referring to this API?
The first issue is the secure communication bit. Reading their website it wants AES-CBC string 128bit with IV inserted at the start then this encoded using base64. i recently created a native extension which used AES-CTR 256bit. So i quickly adapted it and added base64 encoding using defold’s C++ crypt api.
So. this can be added as a dependency.
https://github.com/alfowler1976/aes_cbc_newgrounds/archive/refs/tags/v1.0.0.zip. This will do all the encoding bit for you.
The api implementation can be achieved using defolds http.request function. So the core function is call_component. Then api calls are built around this function. So here is a rough lua module containing a handful of functions.
local M = {}
-- Newgrounds API V3 Gateway URL
local GATEWAY_URL = "https://newgrounds.io/gateway_v3.php"
-- Module state
M.app_id = nil
M.aes_key = nil
M.session_id = nil
function M.init(app_id, aes_key)
M.app_id = app_id
M.aes_key = aes_key
end
function M.get_html5_session_id()
-- synchronously checks HTML5 URL parameters for 'ngio_session_id'
if html5 then
local js_code = "new URLSearchParams(window.location.search).get('ngio_session_id')"
local session_id = html5.run(js_code)
if session_id and session_id ~= "" and session_id ~= "null" then
-- set session
M.session_id = session_id
return session_id
end
end
return nil
end
function M.connect(show_passport_if_guest, callback)
-- tries to get the session 2 ways
-- 1st if html use M.get_html5_session
-- if not get a guest session
-- note: get_html is sync and start session is aync
local html_session = M.get_html5_session_id()
if html_session then
print("HTML5 Session found: " .. html_session)
if callback then callback(true, html_session) end
else
M.start_session(show_passport_if_guest, function(success, session)
if success then
local session_id = session and session.id
print("Started guest session: " .. tostring(session_id))
if callback then callback(true, session_id) end
else
print("Failed to get session.")
if callback then callback(false, session) end
end
end)
end
end
-- start a guest session
function M.start_session(show_passport, callback)
M.call_component("App.startSession", { force = true }, false, function(success, result)
if success then
local session = result.session or (result.data and result.data.session)
-- show passort. allows a user to login against the session
if show_passport and session and session.passport_url then
sys.open_url(session.passport_url)
end
-- store session id
if session and session.id then
M.session_id = session.id
else
pprint("no session section in response")
M.session_id = nil
if callback then callback(false, "no session section in response") end
end
if callback then callback(true, session) end
else
pprint("Failed to start session:", result)
M.session_id = nil
if callback then callback(false, result) end
end
end)
end
function M.check_session_status(callback)
if not M.session_id then
if callback then callback(false, "check_session: session id is required.") end
return
end
M.call_component("App.checkSession", {}, true, function(success, result)
if success then
local session = result.session
if callback then callback(true, session) end
else
if callback then callback(false, result) end
end
end)
end
--- gets medal list. if use_seesion_id ==true and user is logged into the session it will return which medals are unlocked
function M.medals_get(use_session_id, callback)
M.call_component("Medal.getList", {}, use_session_id, function(success, result)
if success then
local medals = result.medals or (result.data and result.data.medals) or {}
if callback then callback(true, medals) end
else
if callback then callback(false, result) end
end
end)
end
--- unlock medal. returns a table of information is successful, if not it returns an error message string
function M.medal_unlock(medal_id, callback)
if not medal_id or not M.session_id then
if callback then callback(false, "Cannot unlock medal: Medal ID and session ID required.") end
return
end
M.call_component("Medal.unlock", { id = medal_id }, true, function(success, result)
if success then
-- note success means data returned it does not mean successful result
if (result.data.success==true) then
-- info is stored in data.medal
local medal = result.data.medal
if callback then callback(true, medal) end
else
local error = result.data.error.message
if callback then callback(false, error) end
end
else
if callback then callback(false, result) end
end
end)
end
--- core HTTP functionality for api call
function M.call_component(component, parameters, requires_session, callback)
-- if not present, return error
if not M.app_id or not M.aes_key then
if callback then callback(false, "Module not initialized") end
return
end
-- check session requirement
if requires_session and not M.session_id then
if callback then callback(false, "No active session ID.") end
return
end
-- create call object
local call_obj = {
component = component,
parameters = parameters or {}
}
local call_json = json.encode(call_obj)
-- encrypt data
local status, encrypted_payload = pcall(aes_cbc.encrypt_newgrounds, call_json, M.aes_key)
if not status or not encrypted_payload then
if callback then callback(false, "Encryption failed: " .. tostring(encrypted_payload)) end
return
end
-- construct request
local request_data = {
app_id = M.app_id,
call = {
secure = encrypted_payload
}
}
if requires_session then
request_data.session_id = M.session_id
end
local boundary = "----DefoldNewgroundsFormBoundary"
local body = "--" .. boundary .. "\r\n"
.. "Content-Disposition: form-data; name=\"input\"\r\n\r\n"
.. json.encode(request_data) .. "\r\n"
.. "--" .. boundary .. "--\r\n"
local headers = {
["Content-Type"] = "multipart/form-data; boundary=" .. boundary
}
http.request(GATEWAY_URL, "POST", function(self, id, http_response)
if http_response.status == 200 or http_response.status == 201 then
local ok, parsed_outer = pcall(json.decode, http_response.response)
if not ok then
if callback then callback(false, "failed to parse server response") end
return
end
-- was return successful
if parsed_outer.success == false then
pprint(parsed_outer.error)
if callback then callback(false, parsed_outer.error or "unknown server error") end
return
end
-- does it have a result section
if parsed_outer.result then
local result = parsed_outer.result
if result.success == false then
local err_msg = result.error or "Unknown component error"
if callback then callback(false, err_msg) end
else
-- Return entire result block to allow wrapper functions to unwrap custom structures
if callback then callback(true, result) end
end
else
if callback then callback(true, parsed_outer) end
end
else
if callback then callback(false, "HTTP Error: " .. tostring(http_response.status)) end
end
end, headers, body)
end
return M
** Because we are using http.request all results are going to be asynchronous. This is very important to consider when coding
So get the app id and encryption id from the game submission page. (note: the encrpytion key is already in base64 format, the native extension expects this).
So the below will set up the module and get a session id
-- intit
newgrounds.init("<your app id", "<your encryption key")
-- async connect
newgrounds.connect(true,
function(success, response)
pprint("session:", success,response)
end
)
Once this was running, i was able to get medals, unlock medals as a quick test. so getting medals was
newgrounds.medals_get(false,function(success, medals)
if success then
pprint("Medals",medals)
else
print("failed")
end
end)
if the session_id is valid, the user is logged and you set the first parameter to true it will also get whether the medal has been unlocked too.
The other interesting thing is that you can run the api without it being run on their server.
if you set the first parameter to true in this function “connect” or “start_session” and its not a html5 app, it will open up a browser for you to log into and then the user is stored against that session id.
So hopefully that is a good start for implementation
I apologise for getting back to the thread so late but this was extremely helpful. Thank you so much