diff --git a/game-example/TsunagiteHandler.cs b/game-example/TsunagiteHandler.cs index dbf51f6..add4120 100644 --- a/game-example/TsunagiteHandler.cs +++ b/game-example/TsunagiteHandler.cs @@ -3,7 +3,7 @@ using Godot; public abstract partial class TsunagiteHandler: Control { //Fill these in with your game data! Will be auto-assembled at submit time. - public const string OwnerHandle = "lemmaeof.gay"; + public const string OwnerDid = "did:plc:fxgreqfae3vqhyjsntsikfux"; public const string GameId = "testGame"; [Signal] diff --git a/game-example/carpanet/CarpaNetHandler.cs b/game-example/carpanet/CarpaNetHandler.cs new file mode 100644 index 0000000..9cd4a0d --- /dev/null +++ b/game-example/carpanet/CarpaNetHandler.cs @@ -0,0 +1,35 @@ +using CarpaNet; +using CarpaNet.OAuth; +using Godot; +using System; +using System.Threading.Tasks; + +public partial class CarpaNetHandler : TsunagiteHandler { + OAuthClientConfig cfg = new() { + ClientId = "https://lemmaeof.gay/oauth-client-metadata.json", + RedirectUri = "http://127.0.0.1:2797/", + Scope = "atproto repo:dev.tsunagite.score?action=create" + }; + + ATProtoOAuthClient client; + + public override async Task Authorize(string handle) { + using OAuthSession sess = new(cfg); + string authUrl = await sess.AuthorizeAsync(handle); + string callbackUrl = ""; + client = await sess.CallbackAsync(callbackUrl); + throw new NotImplementedException(); + } + + public override bool IsAuthorized() { + throw new NotImplementedException(); + } + + public override Task Reauthorize() { + throw new NotImplementedException(); + } + + public override Task SubmitScore(TsunagiteScore score) { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/game-example/hosted/HostedHandler.cs b/game-example/hosted/HostedHandler.cs new file mode 100644 index 0000000..00093e5 --- /dev/null +++ b/game-example/hosted/HostedHandler.cs @@ -0,0 +1,21 @@ +using Godot; +using System; +using System.Threading.Tasks; + +public partial class HostedHandler : TsunagiteHandler { + public override Task Authorize(string handle) { + throw new NotImplementedException(); + } + + public override bool IsAuthorized() { + throw new NotImplementedException(); + } + + public override Task Reauthorize() { + throw new NotImplementedException(); + } + + public override Task SubmitScore(TsunagiteScore score) { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/game-example/hosted/HostedLeaderboardView.cs b/game-example/hosted/HostedLeaderboardView.cs new file mode 100644 index 0000000..35843cc --- /dev/null +++ b/game-example/hosted/HostedLeaderboardView.cs @@ -0,0 +1,6 @@ +using Godot; +using System; + +public partial class HostedLeaderboardView : VBoxContainer { + private const string CLIENT_KEY = "hvc_dfadce196d372aaaaaa0032657ec1bdb"; +} \ No newline at end of file diff --git a/game-example/microcosm/MicrocosmLeaderboardView.cs b/game-example/microcosm/MicrocosmLeaderboardView.cs index 82ebbfc..341ac57 100644 --- a/game-example/microcosm/MicrocosmLeaderboardView.cs +++ b/game-example/microcosm/MicrocosmLeaderboardView.cs @@ -7,7 +7,6 @@ using System.Text.Json; //A leaderboard view assembled using Microcosm's Constellation and Slingshot services without a dedicated Tsunagite server middleware. //This uses a lot of requests overall, which middleware can make less of an issue. -//FIXME: for some reason, Slingshot is taking thirty seconds to respond to *every request!* what's going on?! public partial class MicrocosmLeaderboardView : VBoxContainer { private const string chart = "at://lemmaeof.gay/dev.tsunagite.chart/mirinTestSong-expert"; private static readonly string constellationQuery = $"https://constellation.microcosm.blue/xrpc/blue.microcosm.links.getBacklinks?subject={chart.URIEncode()}&source=dev.tsunagite.score%3Achart&limit=16"; diff --git a/game-example/score/TsunagiteScore.cs b/game-example/score/TsunagiteScore.cs index 26e96cb..38124fe 100644 --- a/game-example/score/TsunagiteScore.cs +++ b/game-example/score/TsunagiteScore.cs @@ -2,6 +2,7 @@ using System; using System.Text.Json.Nodes; using PeterO.Cbor; using System.Collections.Generic; +using System.Globalization; //A score object formatted for Tsunagite. public class TsunagiteScore { @@ -17,16 +18,16 @@ public class TsunagiteScore { public JsonObject ToJson() { JsonObject json = new(); json["$type"] = "dev.tsunagite.score"; - json["game"] = $"at://{TsunagiteHandler.OwnerHandle}/dev.tsunagite.game/{TsunagiteHandler.GameId}"; + json["game"] = $"at://{TsunagiteHandler.OwnerDid}/dev.tsunagite.game/{TsunagiteHandler.GameId}"; if (SongKey.StartsWith("at://")) { json["song"] = SongKey; } else { - json["song"] = $"at://{TsunagiteHandler.OwnerHandle}/dev.tsunagite.song/{SongKey}"; + json["song"] = $"at://{TsunagiteHandler.OwnerDid}/dev.tsunagite.song/{SongKey}"; } if (ChartKey.StartsWith("at://")) { json["chart"] = ChartKey; } else { - json["chart"] = $"at://{TsunagiteHandler.OwnerHandle}/dev.tsunagite.chart/{ChartKey}"; + json["chart"] = $"at://{TsunagiteHandler.OwnerDid}/dev.tsunagite.chart/{ChartKey}"; } json["judgments"] = Judgments.ToJson(); JsonObject scoreComps = new(); @@ -37,6 +38,7 @@ public class TsunagiteScore { scoreComps[element.Key] = comp; } json["scoreComponents"] = scoreComps; + json["scoredAt"] = Timestamp.ToString("o", CultureInfo.InvariantCulture); return json; } diff --git a/schema/permissions.json b/schema/permissions.json new file mode 100644 index 0000000..c496fec --- /dev/null +++ b/schema/permissions.json @@ -0,0 +1,56 @@ +{ + "lexicon": 1, + "id": "dev.tsunagite.permissions", + "description": "Common permission sets used for Tsunagite setups.", + "defs": { + "direct": { + "type": "permission-set", + "description": "Permissions necessary for game using direct-to-PDS interaction.", + "title": "Tsunagite functionality (direct)", + "detail": "Permission to directly upload scores and modify your user profile for this game.", + "permissions": [ + { + "type": "permission", + "resource": "repo", + "collection": [ "dev.tsunagite.score" ], + "action": [ "create" ] + }, + { + "type": "permission", + "resource": "repo", + "collection": [ "dev.tsunagite.profile" ], + "action": [ "create, update" ] + } + ] + }, + "hosted": { + "type": "permission-set", + "description": "Permissions necessary for a game using a hosted service like api.tsunagite.dev.", + "title": "Tsunagite functionality (hosted)", + "detail": "Permission to use a hosted service to upload scores and modify your user profile for this game.", + "permissions": [ + { + "type": "permission", + "resource": "repo", + "collection": [ "dev.tsunagite.score" ], + "action": [ "create" ] + }, + { + "type": "permission", + "resource": "repo", + "collection": [ "dev.tsunagite.profile" ], + "action": [ "create, update" ] + }, + { + "type": "permission", + "resource": "rpc", + "lxm": [ + "dev.tsunagite.submitScore", + "dev.tsunagite.setProfile" + ], + "inheritAud": true + } + ] + } + } +} \ No newline at end of file diff --git a/scripts/game.lua b/scripts/game.lua index bc51c38..45a1530 100644 --- a/scripts/game.lua +++ b/scripts/game.lua @@ -6,7 +6,7 @@ function handle() elseif action == "create" then -- record was created -- TODO: if components get updated later, what do we do about that? - local resp = xrpc.query("com.atproto.identity.resolveIdentity", { identifier = did }) + local resp = http.get("https://slingshot.microcosm.blue/xrpc/blue.microcosm.identity.resolveMiniDoc?identifier=" .. did) local hdl = json.decode(resp.body).handle local tname = makeTableName(hdl, rkey) local query = "CREATE TABLE IF NOT EXISTS '" .. tname .. "' (uri TEXT PRIMARY KEY, did TEXT, chart TEXT, judgments TEXT, version TEXT, " @@ -37,6 +37,7 @@ function handle() end query = query .. "timestamp TEXT);" db.raw(query) + log("creating table " .. tname) log(action .. " " .. uri) end return true diff --git a/scripts/getLeaderboard.lua b/scripts/getLeaderboard.lua index 1b98729..3357dfc 100644 --- a/scripts/getLeaderboard.lua +++ b/scripts/getLeaderboard.lua @@ -31,25 +31,28 @@ function handle() end local query = "SELECT uri, did, MAX(" .. sortBy .. ") " - query = query .. "FROM '" .. tableName .. "' WHERE chart = " .. params.chart .. " " + query = query .. "FROM '" .. tableName .. "' WHERE chart = '" .. params.chart .. "' " if mode then - query = query .. "AND mode = " .. mode .. " " + query = query .. "AND mode = '" .. mode .. "' " end if inputMethod then - query = query .. "AND inputMethod = " .. inputMethod .. " " + query = query .. "AND inputMethod = '" .. inputMethod .. "' " end if versions then if #versions > 1 then query = query .. "AND (" - for _, v in ipairs(versions) do - query = query .. "version = " .. v .. " " + for i, v in ipairs(versions) do + query = query .. "version = '" .. v .. "' " + if i < #versions then + query = query .. "OR " + end end query = query .. ") " else - query = query .. "AND version = " .. versions[1] .. " " + query = query .. "AND version = '" .. versions[1] .. "' " end end - query = query .. "GROUP BY did ORDER BY " .. sortBy .. " DESC;" + query = query .. "GROUP BY did ORDER BY " .. sortBy .. " DESC" local rows = db.raw(query) @@ -57,21 +60,25 @@ function handle() for _, row in ipairs(rows) do local entry = {} - entry["did"] = rows[did] - local resp = xrpc.query("com.atproto.identity.resolveIdentity", { identifier = rows[did] }) + entry["did"] = row.did + local resp = http.get("https://slingshot.microcosm.blue/xrpc/blue.microcosm.identity.resolveMiniDoc?identifier=" .. row.did) entry["handle"] = json.decode(resp.body).handle - entry["profile"] = db.get("at://" .. did .. "/dev.tsunagite.profile/" .. tableName) - entry["score"] = db.get(rows[uri]) + local profile = db.get("at://" .. row.did .. "/dev.tsunagite.profile/" .. tableName) + if profile then + entry["profile"] = profile + end + entry["score"] = db.get(row.uri) + table.insert(result, entry) end - return json.encode(result) + return result end function getTableName(uri) - local trimmed = string.sub("at://did:plc:fxgreqfae3vqhyjsntsikfux/dev.tsunagite.game/testGame", string.len("at://")+1) - local did = string.sub(trimmed, 1, string.find(uri, "/")-1) + local trimmed = string.sub(uri, string.len("at://")+1) + local did = string.sub(trimmed, 1, string.find(trimmed, "/")-1) local _, j = string.find(trimmed, "dev.tsunagite.game/") local key = string.sub(trimmed, j+1, string.len(trimmed)) - local resp = xrpc.query("com.atproto.identity.resolveIdentity", { identifier = did }) + local resp = http.get("https://slingshot.microcosm.blue/xrpc/blue.microcosm.identity.resolveMiniDoc?identifier=" .. did) local hdl = json.decode(resp.body).handle local splitHandle = {} for str in string.gmatch(hdl, "([^.]+)") do diff --git a/scripts/score.lua b/scripts/score.lua index eb104fb..97cb7f3 100644 --- a/scripts/score.lua +++ b/scripts/score.lua @@ -1,6 +1,10 @@ function handle() if action == "delete" then - -- record was deleted + -- record was deleted - remove from leaderboard table too + local oldRecord = db.get(uri) + log("Attempted to find old score record: " .. json.encode(oldRecord)) + local tableName = getTableName(oldRecord.game) + db.raw("DELETE FROM '" .. tableName .. "' WHERE uri = '" .. uri .. "'") log("deleted " .. uri) return true elseif action == "create" then @@ -26,12 +30,14 @@ function handle() if v["$type"] == "dev.tsunagite.score#enum" then -- put enums in as their number local enumComp = game.scoreComponents[k] - for _, e in ipairs(enumComp) do + local foundValues = "" + for _, e in ipairs(enumComp.values) do + foundValues = foundValues .. e.id .. "(" .. e.index .. ") " if e.id == v.value then valueOrder = valueOrder .. e.index .. ", " - break end end + log("Attempted to find enum value " .. v.value .. " among enum set :" .. foundValues) elseif v["$type"] == "dev.tsunagite.score#text" then -- put strings in quotes valueOrder = valueOrder .. "'" .. v.value .. "', " @@ -42,18 +48,21 @@ function handle() end columnOrder = columnOrder .. "timestamp)" valueOrder = valueOrder .. "'" .. record.scoredAt .. "')" - db.raw("INSERT INTO '$1' $2 VALUES $3;", {tableName, columnOrder, valueOrder}) + local query = "INSERT INTO '" .. tableName .. "' " .. columnOrder .. " VALUES " .. valueOrder + log("Record submission query: " .. query) + db.raw(query) + --db.raw("INSERT INTO '$1' $2 VALUES $3;", {tableName, columnOrder, valueOrder}) log(action .. " " .. uri) return true end end function getTableName(uri) - local trimmed = string.sub("at://did:plc:fxgreqfae3vqhyjsntsikfux/dev.tsunagite.game/testGame", string.len("at://")+1) - local did = string.sub(trimmed, 1, string.find(uri, "/")-1) + local trimmed = string.sub(uri, string.len("at://")+1) + local did = string.sub(trimmed, 1, string.find(trimmed, "/")-1) local _, j = string.find(trimmed, "dev.tsunagite.game/") local key = string.sub(trimmed, j+1, string.len(trimmed)) - local resp = xrpc.query("com.atproto.identity.resolveIdentity", { identifier = did }) + local resp = http.get("https://slingshot.microcosm.blue/xrpc/blue.microcosm.identity.resolveMiniDoc?identifier=" .. did) local hdl = json.decode(resp.body).handle local splitHandle = {} for str in string.gmatch(hdl, "([^.]+)") do diff --git a/oauth-client-metadata.json b/site/native-client-metadata.json similarity index 54% rename from oauth-client-metadata.json rename to site/native-client-metadata.json index 0c76e4c..ac66dd9 100644 --- a/oauth-client-metadata.json +++ b/site/native-client-metadata.json @@ -1,8 +1,8 @@ { - "client_name": "Tsunagite Test", - "client_id": "https://lemmaeof.gay/oauth-client-metadata.json", + "client_name": "Tsunagite game integration", + "client_id": "https://tsunagite.dev/native-client-metadata.json", "redirect_uris": ["http://127.0.0.1"], - "scope": "atproto repo:dev.tsunagite.score?action=create transition:generic", + "scope": "atproto include:dev.tsunagite.permissions%23hosted?aud=api.tsunagite.dev", "application_type": "native", "response_types": ["code"], "grant_types": ["authorization_code", "refresh_token"],