Newgrounds API for Defold

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

3 Likes

I apologise for getting back to the thread so late but this was extremely helpful. Thank you so much

1 Like