mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-10 02:08:29 -04:00
226 lines
6.7 KiB
Lua
226 lines
6.7 KiB
Lua
local Json = require("src.link.Json")
|
|
|
|
local SyncClient = {}
|
|
SyncClient.__index = SyncClient
|
|
|
|
SyncClient.DEFAULT_URL = os.getenv("POKEPORT_SYNC_URL")
|
|
or "https://sync.147.182.215.255.sslip.io"
|
|
SyncClient.MAX_BLOB = 2 * 1024 * 1024
|
|
SyncClient.MAX_RESPONSE = 4 * 1024 * 1024
|
|
SyncClient.TIMEOUT = 25
|
|
|
|
function SyncClient.normalizeCode(code)
|
|
if type(code) ~= "string" and type(code) ~= "number" then return nil end
|
|
local digits = tostring(code):gsub("[^%d]", "")
|
|
if #digits ~= 8 then return nil end
|
|
return digits
|
|
end
|
|
|
|
function SyncClient.formatCode(code)
|
|
local digits = SyncClient.normalizeCode(code)
|
|
if not digits then return nil end
|
|
return digits:sub(1, 4) .. "-" .. digits:sub(5, 8)
|
|
end
|
|
|
|
local function escape(s)
|
|
return (tostring(s):gsub("[^%w%-%._~]", function(c)
|
|
return ("%%%02X"):format(c:byte())
|
|
end))
|
|
end
|
|
|
|
local function query(params)
|
|
local names = {}
|
|
for name in pairs(params or {}) do names[#names + 1] = tostring(name) end
|
|
table.sort(names)
|
|
local out = {}
|
|
for _, name in ipairs(names) do
|
|
out[#out + 1] = escape(name) .. "=" .. escape(params[name])
|
|
end
|
|
if #out == 0 then return "" end
|
|
return "?" .. table.concat(out, "&")
|
|
end
|
|
|
|
function SyncClient.new(opts)
|
|
opts = opts or {}
|
|
local base = opts.baseUrl or SyncClient.DEFAULT_URL
|
|
base = tostring(base):gsub("/+$", "")
|
|
local transport = opts.transport
|
|
if not transport then
|
|
transport = require("src.sync.SyncTransport").new()
|
|
end
|
|
return setmetatable({
|
|
baseUrl = base,
|
|
transport = transport,
|
|
account = opts.account,
|
|
token = opts.token,
|
|
}, SyncClient)
|
|
end
|
|
|
|
function SyncClient:setAuth(account, token)
|
|
self.account = type(account) == "string" and account ~= "" and account or nil
|
|
self.token = type(token) == "string" and token ~= "" and token or nil
|
|
end
|
|
|
|
function SyncClient:clearAuth()
|
|
self.account, self.token = nil, nil
|
|
end
|
|
|
|
function SyncClient:isLinked()
|
|
return self.account ~= nil and self.token ~= nil
|
|
end
|
|
|
|
function SyncClient:send(method, path, body, opts)
|
|
opts = opts or {}
|
|
local headers = { ["Accept"] = "application/json" }
|
|
local payload
|
|
if body ~= nil then
|
|
if type(body) == "table" and next(body) == nil then
|
|
payload = "{}"
|
|
else
|
|
local ok, encoded = pcall(Json.encode, body)
|
|
if not ok then return nil, "could not encode the request" end
|
|
payload = encoded
|
|
end
|
|
headers["Content-Type"] = "application/json"
|
|
end
|
|
if not opts.noAuth then
|
|
if not self:isLinked() then return nil, "this device is not linked" end
|
|
headers["x-sync-account"] = self.account
|
|
headers["x-sync-token"] = self.token
|
|
end
|
|
local url = self.baseUrl .. path .. query(opts.params)
|
|
local handle = self.transport:begin({
|
|
url = url, method = method, body = payload, headers = headers,
|
|
maxSeconds = opts.maxSeconds or SyncClient.TIMEOUT,
|
|
})
|
|
if handle == nil then return nil, "no network transport" end
|
|
return handle
|
|
end
|
|
|
|
function SyncClient:poll(handle)
|
|
if handle == nil then return { status = "error", err = "no request" } end
|
|
local res = self.transport:poll(handle)
|
|
if res.status == "pending" then return { status = "pending" } end
|
|
if res.status ~= "ok" then
|
|
return { status = "error", err = res.err or "sync request failed" }
|
|
end
|
|
local raw = res.body or ""
|
|
local code = tonumber(res.code) or 0
|
|
if #raw > SyncClient.MAX_RESPONSE then
|
|
return { status = "error", code = code, err = "the reply was too large" }
|
|
end
|
|
local data, decodeErr = Json.decode(raw, SyncClient.MAX_RESPONSE)
|
|
if type(data) ~= "table" then
|
|
local why = Json.describeUnexpected(raw) or decodeErr or "unreadable reply"
|
|
if code >= 400 then
|
|
return { status = "error", code = code,
|
|
err = ("the server answered %d"):format(code) }
|
|
end
|
|
return { status = "error", code = code, err = why }
|
|
end
|
|
if code >= 400 or data.error then
|
|
local err = data.error
|
|
if type(err) ~= "string" or err == "" then
|
|
err = ("the server answered %d"):format(code)
|
|
end
|
|
return { status = "error", code = code, data = data, err = err }
|
|
end
|
|
return { status = "ok", code = code, data = data }
|
|
end
|
|
|
|
function SyncClient:release(handle)
|
|
if handle ~= nil then self.transport:release(handle) end
|
|
end
|
|
|
|
function SyncClient:create(deviceLabel)
|
|
return self:send("POST", "/sync/create",
|
|
{ device = deviceLabel or "device" }, { noAuth = true })
|
|
end
|
|
|
|
function SyncClient:link(code1, code2, deviceLabel)
|
|
local a = SyncClient.normalizeCode(code1)
|
|
local b = SyncClient.normalizeCode(code2)
|
|
if not a or not b then return nil, "both codes are 8 digits" end
|
|
return self:send("POST", "/sync/link",
|
|
{ code1 = a, code2 = b, device = deviceLabel or "device" },
|
|
{ noAuth = true })
|
|
end
|
|
|
|
function SyncClient:fetchState()
|
|
return self:send("GET", "/sync/state")
|
|
end
|
|
|
|
function SyncClient:putSave(entry)
|
|
if type(entry) ~= "table" then return nil, "missing save entry" end
|
|
if type(entry.blob) ~= "string" or entry.blob == "" then
|
|
return nil, "missing save data"
|
|
end
|
|
if #entry.blob > SyncClient.MAX_BLOB then
|
|
return nil, "this save is too large to sync"
|
|
end
|
|
return self:send("PUT", "/sync/save", {
|
|
version = entry.version,
|
|
slot = entry.slot,
|
|
meta = entry.meta,
|
|
blob = entry.blob,
|
|
baseRev = entry.baseRev,
|
|
force = entry.force and true or nil,
|
|
})
|
|
end
|
|
|
|
function SyncClient:getSave(version, id)
|
|
return self:send("GET", "/sync/save", nil,
|
|
{ params = { version = version, id = id } })
|
|
end
|
|
|
|
function SyncClient:deleteSave(version, id, rev)
|
|
return self:send("DELETE", "/sync/save", nil,
|
|
{ params = { version = version, id = id, rev = rev } })
|
|
end
|
|
|
|
function SyncClient:putMods(manifest)
|
|
return self:send("PUT", "/sync/mods", { manifest = manifest })
|
|
end
|
|
|
|
function SyncClient:getMods()
|
|
return self:send("GET", "/sync/mods")
|
|
end
|
|
|
|
function SyncClient:shareMods(manifest)
|
|
return self:send("POST", "/sync/modshare", { manifest = manifest })
|
|
end
|
|
|
|
function SyncClient:fetchShare(code)
|
|
local trimmed = tostring(code or ""):gsub("%s", ""):upper()
|
|
if not trimmed:match("^[A-Z2-9]+$") or #trimmed ~= 6 then
|
|
return nil, "share codes are 6 characters"
|
|
end
|
|
return self:send("GET", "/sync/modshare", nil,
|
|
{ noAuth = true, params = { code = trimmed } })
|
|
end
|
|
|
|
function SyncClient:lobbyTicket(displayName)
|
|
local body = {}
|
|
if type(displayName) == "string" and displayName ~= "" then
|
|
body.displayName = displayName
|
|
end
|
|
return self:send("POST", "/lobby/ticket", body)
|
|
end
|
|
|
|
function SyncClient:setDisplayName(name)
|
|
if type(name) ~= "string" or name == "" then
|
|
return nil, "pick a display name first"
|
|
end
|
|
return self:send("POST", "/sync/displayname", { displayName = name })
|
|
end
|
|
|
|
function SyncClient:reissueCodes()
|
|
return self:send("POST", "/sync/codes")
|
|
end
|
|
|
|
function SyncClient:unlink(device)
|
|
return self:send("POST", "/sync/unlink", { device = device })
|
|
end
|
|
|
|
return SyncClient
|