galvanic corrosion rewrite

commit this before something goes horribly wrong
This commit is contained in:
2025-08-12 21:04:52 -04:00
parent 941c8400c0
commit f19552929e
40 changed files with 28149 additions and 73212 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@
"m_Enabled": 1, "m_Enabled": 1,
"m_Script": { "m_Script": {
"m_FileID": 1, "m_FileID": 1,
"m_PathID": 5244 "m_PathID": 3147
}, },
"m_Name": "EquipmentWardrobeRuntimeConfig", "m_Name": "EquipmentWardrobeRuntimeConfig",
"toolSkinMaps": [ "toolSkinMaps": [
@@ -75,14 +75,6 @@
{ {
"skinAssetName": "PaintballGun_Skin_Pirate", "skinAssetName": "PaintballGun_Skin_Pirate",
"skinGuid": "b8d5612b-2c6f-46d6-9412-decddac7d4c1" "skinGuid": "b8d5612b-2c6f-46d6-9412-decddac7d4c1"
},
{
"skinAssetName": "PaintballGun_Skin_Caution",
"skinGuid": "Dg9PFcg-JUia72PnYJqkQg"
},
{
"skinAssetName": "PaintballGun_Skin_Valentines",
"skinGuid": "z0Xw8_BUZUODrGHSZcLHHQ"
} }
] ]
}, },
@@ -107,10 +99,6 @@
{ {
"skinAssetName": "PaintballShotgun_Watermeon_Skin", "skinAssetName": "PaintballShotgun_Watermeon_Skin",
"skinGuid": "ZMAx_-B_7kWy6-TLXx8g6Q" "skinGuid": "ZMAx_-B_7kWy6-TLXx8g6Q"
},
{
"skinAssetName": "PaintballShotgun_Skin_Caution",
"skinGuid": "CwIBKjm3G0-xUGt_9Gf7UQ"
} }
] ]
}, },
@@ -143,10 +131,6 @@
{ {
"skinAssetName": "Basketball_Skin_Cozy", "skinAssetName": "Basketball_Skin_Cozy",
"skinGuid": "WOWwmg7jg0mH2g4LJUp6fA" "skinGuid": "WOWwmg7jg0mH2g4LJUp6fA"
},
{
"skinAssetName": "Basketball_Skin_Gold",
"skinGuid": "bNEcMJOzokeEVKDeXk_cDQ"
} }
] ]
}, },
@@ -175,14 +159,6 @@
{ {
"skinAssetName": "PaintballShield_Skin_Gingerbread", "skinAssetName": "PaintballShield_Skin_Gingerbread",
"skinGuid": "QTyRLpDB3UuReHQqrKxb5A" "skinGuid": "QTyRLpDB3UuReHQqrKxb5A"
},
{
"skinAssetName": "PaintballShield_Plaid_Skin",
"skinGuid": "gfGhbEMx_kCRdO-6vPOh3g"
},
{
"skinAssetName": "PaintballShield_Skin_Caution",
"skinGuid": "ClJApV0LC0mxh0UiYhaXGQ"
} }
] ]
}, },
@@ -223,22 +199,6 @@
{ {
"skinAssetName": "QuestShield_Skin_Pride", "skinAssetName": "QuestShield_Skin_Pride",
"skinGuid": "439be0eb-4d1e-4c80-97f8-b4636d0ce94b" "skinGuid": "439be0eb-4d1e-4c80-97f8-b4636d0ce94b"
},
{
"skinAssetName": "QuestShield_Skin_Plaid",
"skinGuid": "k0BVix46IEmgVrqUmFU3-Q"
},
{
"skinAssetName": "QuestShield_Skin_Caution",
"skinGuid": "RQCgWA3Pf0KTqznUR58-lw"
},
{
"skinAssetName": "QuestShield_Skin_Snowflake",
"skinGuid": "-rbKD_ztMUq69Nv4tLrrVA"
},
{
"skinAssetName": "QuestShield_Skin_Neon_RR+",
"skinGuid": "L9usPszLFkiA6xs32es2Lw"
} }
] ]
}, },
@@ -271,14 +231,6 @@
{ {
"skinAssetName": "Quest_SciFi_Pistol_Recasso_Skin", "skinAssetName": "Quest_SciFi_Pistol_Recasso_Skin",
"skinGuid": "0cFcVYCLTU6CKhr9uADrbw" "skinGuid": "0cFcVYCLTU6CKhr9uADrbw"
},
{
"skinAssetName": "Quest_SciFi_Pistol_Plaid_Skin",
"skinGuid": "aByJShU520OzdOXBV8o0Dg"
},
{
"skinAssetName": "Quest_SciFi_Pistol_Gold_Skin",
"skinGuid": "dRvcMuM__02iMsRwKEPtXQ"
} }
] ]
}, },
@@ -303,10 +255,6 @@
{ {
"skinAssetName": "Quest_SciFi_AutomaticGun_Zombie", "skinAssetName": "Quest_SciFi_AutomaticGun_Zombie",
"skinGuid": "dEmh0tniCkeYBY1EdD09jA" "skinGuid": "dEmh0tniCkeYBY1EdD09jA"
},
{
"skinAssetName": "Quest_SciFi_AutomaticGun_Plaid_Skin",
"skinGuid": "jzgz_HrPAUqOeqx3uNXVUA"
} }
] ]
}, },
@@ -331,18 +279,6 @@
{ {
"skinAssetName": "Quest_SciFi_Shotgun_Skin_Valentine", "skinAssetName": "Quest_SciFi_Shotgun_Skin_Valentine",
"skinGuid": "cd497ae2-6382-42f2-9f92-93d5141588c3" "skinGuid": "cd497ae2-6382-42f2-9f92-93d5141588c3"
},
{
"skinAssetName": "Quest_SciFi_Shotgun_Plaid_Skin",
"skinGuid": "pIRihZheSEW2Ld3VTLkB-g"
},
{
"skinAssetName": "Quest_SciFi_Shotgun_Skin_Shamrock",
"skinGuid": "gjuFgDp-LUWPi1mPCKyqbA"
},
{
"skinAssetName": "Quest_SciFi_Shotgun_Skin_Galaxy",
"skinGuid": "xHEZCBn5D0iUxIDp3swd9g"
} }
] ]
}, },
@@ -363,14 +299,6 @@
{ {
"skinAssetName": "Quest_SciFi_RailGun_LifeGuard_Skin", "skinAssetName": "Quest_SciFi_RailGun_LifeGuard_Skin",
"skinGuid": "X9gTAbp0Sk6nadpTBseRrg" "skinGuid": "X9gTAbp0Sk6nadpTBseRrg"
},
{
"skinAssetName": "Quest_SciFi_RailGun_Plaid_Skin",
"skinGuid": "_flhynGnykyZL7F83B7rJQ"
},
{
"skinAssetName": "Quest_SciFi_RailGun_Skin_Galaxy",
"skinGuid": "uvbozCned0igzscgeBlRpQ"
} }
] ]
}, },
@@ -399,34 +327,6 @@
{ {
"skinAssetName": "Crossbow_Skin_Bone", "skinAssetName": "Crossbow_Skin_Bone",
"skinGuid": "T6PzFfg41UaQ7EAqts6gsw" "skinGuid": "T6PzFfg41UaQ7EAqts6gsw"
},
{
"skinAssetName": "Crossbow_Skin_Dryad",
"skinGuid": "jHaoDSAtikyAI82lO53o9g"
},
{
"skinAssetName": "Crossbow_Skin_Plaid",
"skinGuid": "ESQ0qOlgrkCc8MbUXGnF8A"
},
{
"skinAssetName": "Crossbow_Skin_Rock",
"skinGuid": "QEwDZYj_OEi5l86LBnuYGw"
},
{
"skinAssetName": "Crossbow_Skin_Caution",
"skinGuid": "VHFKZVhiRkylcxnxzCKUuw"
},
{
"skinAssetName": "Crossbow_Dryad_Fall_Skin",
"skinGuid": "Ys9UQ5b7fEyKxnaek-ihfQ"
},
{
"skinAssetName": "Crossbow_Skin_Winter",
"skinGuid": "CZAIad-ME0O0iIy77MRxBw"
},
{
"skinAssetName": "Crossbow_Skin_Spring",
"skinGuid": "ych0ntYcK0eiq67bCLlbew"
} }
] ]
}, },
@@ -451,30 +351,6 @@
{ {
"skinAssetName": "Longbow_Skin_Rainbow", "skinAssetName": "Longbow_Skin_Rainbow",
"skinGuid": "XpxqrY6RYkafs-sd3Z0ZLw" "skinGuid": "XpxqrY6RYkafs-sd3Z0ZLw"
},
{
"skinAssetName": "Longbow_Dryad_Skin",
"skinGuid": "JBp7CxPhrUC5hnn2EISUFA"
},
{
"skinAssetName": "Longbow_Skin_Plaid",
"skinGuid": "T8666vuehkObksBdVbla9g"
},
{
"skinAssetName": "Longbow_Skin_Caution",
"skinGuid": "YajcGvBcTEaf_MjCbxOciw"
},
{
"skinAssetName": "Longbow_Dryad_Fall_Skin",
"skinGuid": "XKFuhM3zikyxTYtTzuPb_A"
},
{
"skinAssetName": "Longbow_Skin_Winter",
"skinGuid": "uPwTBnfWwkWXuyAaAMqeQA"
},
{
"skinAssetName": "Longbow_Skin_Spring",
"skinGuid": "gl_tlo_t7U-H308Omy13lw"
} }
] ]
}, },
@@ -511,74 +387,6 @@
{ {
"skinAssetName": "MakerPen_Professor_Skin", "skinAssetName": "MakerPen_Professor_Skin",
"skinGuid": "4HJ3wRmSZUuhMRUTA67dpA" "skinGuid": "4HJ3wRmSZUuhMRUTA67dpA"
},
{
"skinAssetName": "MakerPen_Skin_Wonderland",
"skinGuid": "i3NgZvTwkUSMsXRpqCgSZA"
},
{
"skinAssetName": "MakerPen_Skin_Beach",
"skinGuid": "BGhlN3FKGEy6w-vwc28V4A"
},
{
"skinAssetName": "MakerPen_Skin_Mystery",
"skinGuid": "BwIscV_YnE6iG4brl389Hg"
},
{
"skinAssetName": "MakerPen_Skin_MovieMagic",
"skinGuid": "JtMeQRvo00yVwAliICTP0w"
},
{
"skinAssetName": "MakerPen_Skin_Host1",
"skinGuid": "h4xKGsHcTUy3iH7PVFjb2w"
},
{
"skinAssetName": "MakerPen_Skin_Host2",
"skinGuid": "cUBvDvybvEu5eZDPn57ExQ"
},
{
"skinAssetName": "MakerPen_Skin_Green",
"skinGuid": "VYF9NGZBgU6TOo9ayjgBEQ"
},
{
"skinAssetName": "MakerPen_Skin_Orange",
"skinGuid": "WwtFcRluQkmnCI_qH75L_Q"
},
{
"skinAssetName": "MakerPen_Skin_Purple",
"skinGuid": "iRtlLKI-X0S9oYUWLenAKQ"
},
{
"skinAssetName": "MakerPen_Skin_Red",
"skinGuid": "tvjYGeRdwEqnr9mf84WgcQ"
},
{
"skinAssetName": "MakerPen_Skin_Yellow",
"skinGuid": "Q7Tr_0DXAkOdr61UXiocow"
},
{
"skinAssetName": "MakerPen_Skin_Pink",
"skinGuid": "c1VRbmDGHUS7KiSgwj5prQ"
},
{
"skinAssetName": "MakerPen_Skin_White",
"skinGuid": "MODdVYrjaEy-iy6quALNuA"
},
{
"skinAssetName": "MakerPen_Skin_HiddenWorlds",
"skinGuid": "H91DVbHzR06oie8fLjdE1A"
},
{
"skinAssetName": "MakerPen_Skin_Carnival",
"skinGuid": "RPWXRfZwfUeC9PFlvcbmxw"
},
{
"skinAssetName": "MakerPen_Skin_Gold",
"skinGuid": "j83FD3aSQ0OUcrp4QIIZsA"
},
{
"skinAssetName": "MakerPen_ContestStranded_Skin",
"skinGuid": "L12C6OfxB0y53bLXZQ4rFg"
} }
] ]
}, },
@@ -607,14 +415,6 @@
{ {
"skinAssetName": "PaintballRifleScoped_Comic_Skin", "skinAssetName": "PaintballRifleScoped_Comic_Skin",
"skinGuid": "0dM2SfqGR0SmtO5ufTWfUQ" "skinGuid": "0dM2SfqGR0SmtO5ufTWfUQ"
},
{
"skinAssetName": "PaintballRifleScoped_Skin_Wood",
"skinGuid": "btB2z7ybskSYrn9Nzhfhxg"
},
{
"skinAssetName": "PaintballRifleScoped_Skin_Caution",
"skinGuid": "b_iiMMYayU-an7hOlCzPIA"
} }
] ]
}, },
@@ -627,18 +427,6 @@
{ {
"skinAssetName": "Crossbow_Hunter_Skin_Rock", "skinAssetName": "Crossbow_Hunter_Skin_Rock",
"skinGuid": "d217ee4c-22f3-4f33-bf7c-9d4ee9c30e29" "skinGuid": "d217ee4c-22f3-4f33-bf7c-9d4ee9c30e29"
},
{
"skinAssetName": "Crossbow_Hunter_Skin_Plaid",
"skinGuid": "cFMh54GoHEeHZEERSRR8kQ"
},
{
"skinAssetName": "Crossbow_Hunter_Skin_Caution",
"skinGuid": "RRCvths6eUmAAr9hzGFT0g"
},
{
"skinAssetName": "Crossbow_SharkHunter_Skin",
"skinGuid": "-X_Hm6dIekqoHTY2VMMLeA"
} }
] ]
}, },
@@ -683,26 +471,6 @@
{ {
"skinAssetName": "QuestSword_Skin_CornCob", "skinAssetName": "QuestSword_Skin_CornCob",
"skinGuid": "wioj1rR1lkCx7f-oiCHcOw" "skinGuid": "wioj1rR1lkCx7f-oiCHcOw"
},
{
"skinAssetName": "QuestSword_Plaid_Skin",
"skinGuid": "m1JMjJYpSUiXj5897orm2Q"
},
{
"skinAssetName": "QuestSword_Skin_Caution",
"skinGuid": "d97pbQMk9UWpEN0yoOE9Rg"
},
{
"skinAssetName": "QuestSword_Skin_Lava",
"skinGuid": "DRXoMS9phEG2fkasja7sEw"
},
{
"skinAssetName": "QuestSword_Skin_GoldenSword",
"skinGuid": "uLNdhrrAC0ybxC2XkBnnVQ"
},
{
"skinAssetName": "QuestSword_Skin_Neon_RR+",
"skinGuid": "-ybFpUxTsUKWFSIS-8fOOw"
} }
] ]
}, },
@@ -735,26 +503,6 @@
{ {
"skinAssetName": "Quest_Goblin_Wand_Ice_Skin", "skinAssetName": "Quest_Goblin_Wand_Ice_Skin",
"skinGuid": "3pE7zA-DjkqP1Ch7__-31w" "skinGuid": "3pE7zA-DjkqP1Ch7__-31w"
},
{
"skinAssetName": "Quest_Goblin_Wand_Wood_Skin",
"skinGuid": "m_otHTEKF0KQljsOc-JENg"
},
{
"skinAssetName": "Quest_Goblin_Wand_Skin_Plaid",
"skinGuid": "CyrdBvIbXUq_TsXFliWk1A"
},
{
"skinAssetName": "Quest_Goblin_Wand_Skin_Caution",
"skinGuid": "vfzrRHn24E67BK8Bgy60xA"
},
{
"skinAssetName": "Quest_Goblin_Wand_Trident_Skin",
"skinGuid": "QnLM4Qw-L0ml_mgReFweIw"
},
{
"skinAssetName": "Quest_Goblin_Wand_Butterfly_Skin",
"skinGuid": "M6BHcdDZnU6UEDa3wLUUhg"
} }
] ]
}, },
@@ -779,18 +527,6 @@
{ {
"skinAssetName": "PaintballGrenadeLauncher_Honey_Skin", "skinAssetName": "PaintballGrenadeLauncher_Honey_Skin",
"skinGuid": "3UmIhvqmkU-aWxFDFd4QDg" "skinGuid": "3UmIhvqmkU-aWxFDFd4QDg"
},
{
"skinAssetName": "PaintballGrenadeLauncher_Skin_EasterEgg",
"skinGuid": "pPpYY9j0Dku2gtbKoUOCCg"
},
{
"skinAssetName": "PaintballGrenadeLauncher_Skin_Wood",
"skinGuid": "9gBl778RvUeSC8837Gurog"
},
{
"skinAssetName": "PaintballGrenadeLauncher_Skin_Caution",
"skinGuid": "tcMMGKcmhkuM82ISCZ-3AQ"
} }
] ]
}, },
@@ -811,14 +547,6 @@
{ {
"skinAssetName": "DodgeballBall_Goblin_Skin", "skinAssetName": "DodgeballBall_Goblin_Skin",
"skinGuid": "f55dda45-c17e-4237-a638-9f326d306e7d" "skinGuid": "f55dda45-c17e-4237-a638-9f326d306e7d"
},
{
"skinAssetName": "DodgeballBall_Skin_Gold",
"skinGuid": "Jkg-OGXdIUyfEwaQIh6EGA"
},
{
"skinAssetName": "DodgeballBall_Skin_Pumpkin",
"skinGuid": "lIpyvEMWqUyv41gTp3YZ1A"
} }
] ]
}, },
@@ -867,10 +595,6 @@
{ {
"skinAssetName": "RecRoyale_Backpack_Skin_FallLeaves", "skinAssetName": "RecRoyale_Backpack_Skin_FallLeaves",
"skinGuid": "70uy5UJhhEy1aynKM4MAsQ" "skinGuid": "70uy5UJhhEy1aynKM4MAsQ"
},
{
"skinAssetName": "RecRoyale_Backpack_Gold_Skin",
"skinGuid": "ujZ4Hl0HO06OysqZcqHhmg"
} }
] ]
}, },
@@ -911,14 +635,6 @@
{ {
"skinAssetName": "ShareCamera_Skin_Heart", "skinAssetName": "ShareCamera_Skin_Heart",
"skinGuid": "QQALCzCF-0ClDWqmmciHAQ" "skinGuid": "QQALCzCF-0ClDWqmmciHAQ"
},
{
"skinAssetName": "ShareCamera_Skin_Caution",
"skinGuid": "SkTa53OzM0u82YPuZAl9aw"
},
{
"skinAssetName": "ShareCamera_Skin_Plaid",
"skinGuid": "sE4_-ZVa2EC4MZfGGsExzQ"
} }
] ]
}, },
@@ -1099,82 +815,6 @@
{ {
"skinAssetName": "PaintballAssaultRifle_Skin_wood", "skinAssetName": "PaintballAssaultRifle_Skin_wood",
"skinGuid": "357fe573-fee7-467f-93a7-5e61afb024b8" "skinGuid": "357fe573-fee7-467f-93a7-5e61afb024b8"
},
{
"skinAssetName": "PaintballAssaultRifle_Skin_Ghost",
"skinGuid": "2vhCtZjRd0i_nYo04ij__w"
}
]
},
{
"equipment": {
"prefabName": "[PaintballGun] Confetti",
"toolAssetName": "[PaintballGun] Confetti"
},
"skins": [
{
"skinAssetName": "PaintballGunConfetti_Skin_Gold",
"skinGuid": "bfrFOdnHzEaIwHqem2dXkg"
}
]
},
{
"equipment": {
"prefabName": "[Paintball_PaintThrower]",
"toolAssetName": "[Paintball_PaintThrower]"
},
"skins": [
{
"skinAssetName": "Paintball_PaintThrower_Skin_Wood",
"skinGuid": "y_FesIT5jkmb61JIu7tfsw"
},
{
"skinAssetName": "Paintball_PaintThrower_Plaid_Skin",
"skinGuid": "GbHNkVbVgUCoirMhOGkSTA"
},
{
"skinAssetName": "Paintball_PaintThrower_Skin_Caution",
"skinGuid": "qhhhJeHVx0KLnGJrAGIn9g"
},
{
"skinAssetName": "Paintball_PaintThrower_Skin_Fireworks",
"skinGuid": "OFEdf4dRC0a3nHugXVOXnA"
},
{
"skinAssetName": "Paintball_PaintThrower_Skin_DoubleDrencher",
"skinGuid": "VTVy8cweC0K2ca1nTXMu0g"
},
{
"skinAssetName": "Paintball_PaintThrower_Skin_CandyCane",
"skinGuid": "0YfCdfAt2kiN0LQcPKSm_Q"
}
]
},
{
"equipment": {
"prefabName": "[BowlingBall]",
"toolAssetName": "[BowlingBall]"
},
"skins": [
{
"skinAssetName": "BowlingBall_Skin_Gold",
"skinGuid": "BvUZUamUh0uDbjDOrQrX1A"
}
]
},
{
"equipment": {
"prefabName": "[Bucket]",
"toolAssetName": "[Bucket]"
},
"skins": [
{
"skinAssetName": "Bucket_BlackLight_Swirl_Skin",
"skinGuid": "VW3WV6oQOkqYshf1xc9n3A"
},
{
"skinAssetName": "Bucket_Fashion_Skin",
"skinGuid": "ou23DX__T0qFPjAXC5WrOQ"
} }
] ]
} }

File diff suppressed because it is too large Load Diff

View File

@@ -7,17 +7,23 @@ import Command from "./server/commands/command.ts";
import { ServerContentBase } from "./server/ContentBase.ts"; import { ServerContentBase } from "./server/ContentBase.ts";
import z from "zod"; import z from "zod";
import { verify } from "@hono/hono/jwt"; import { verify } from "@hono/hono/jwt";
import { type ProfileToken } from "./server/profiles/types/profile.ts";
import { SignalRSocketHandler } from "./server/socket/signalr/socket.ts"; import { SignalRSocketHandler } from "./server/socket/signalr/socket.ts";
import { PushNotificationId } from "./server/socket/signalr/types.ts"; import { PushNotificationId } from "./server/socket/signalr/types.ts";
import { genericResponse } from "./util/api.ts"; import { genericResponse } from "./util/api.ts";
import { getNetConfig } from "./net.ts";
import { TokenFormat, TokenType } from "./server/platforms/types.ts";
LoggingConfiguration.resetTimeFormat = TimeFormat.Unix; LoggingConfiguration.resetTimeFormat = TimeFormat.Unix;
LoggingConfiguration.resetLogTiming = LogTiming.Microtask; LoggingConfiguration.resetLogTiming = LogTiming.Microtask;
const log = new Logging("Main"); const log = new Logging("Main");
log.i(`wsi by zombieb`); log.i(`Galvanic Corrosion rewritten`);
if (Deno.args.includes('--flush-persistence')) {
await Deno.remove('./persist', { recursive: true });
log.w(`Persistence was wiped!`);
}
export function detailedLog(items: (string | number | boolean | null)[]) { export function detailedLog(items: (string | number | boolean | null)[]) {
return items.filter(val => val !== null).join('\r\n '); return items.filter(val => val !== null).join('\r\n ');
@@ -56,11 +62,14 @@ const onListen = async () => {
Object.values(Server).forEach(base => ((base as ServerContentBase).start ? (base as ServerContentBase).start() : undefined)); Object.values(Server).forEach(base => ((base as ServerContentBase).start ? (base as ServerContentBase).start() : undefined));
} }
const server = Deno.serve({ hostname: "10.0.1.39", port: 13370, onListen: addr => { const netConfig = getNetConfig();
log.n(`Listening info: ${JSON.stringify(addr)}`); const server = Deno.serve({
hostname: netConfig.host, port: netConfig.port, onListen: addr => {
log.n(`Listening info: ${JSON.stringify(addr)}`);
onListen(); onListen();
}}, async (req, info) => { }
}, async (req, info) => {
const url = new URL(req.url); const url = new URL(req.url);
const srcAddr = getSourceAddress(req, info.remoteAddr); const srcAddr = getSourceAddress(req, info.remoteAddr);
@@ -68,9 +77,9 @@ const server = Deno.serve({ hostname: "10.0.1.39", port: 13370, onListen: addr =
if (url.pathname == '/notify/hub/v1/negotiate') { if (url.pathname == '/notify/hub/v1/negotiate') {
return new Response(JSON.stringify({ return new Response(JSON.stringify({
connectionId: "who_said_it", connectionId: "galv4",
availableTransports: [{transport:"WebSockets",transferFormats:["Text"]}] availableTransports: [{ transport: "WebSockets", transferFormats: ["Text"] }]
}), { headers: { 'Content-Type': 'application/json' }}); }), { headers: { 'Content-Type': 'application/json' } });
} }
if (req.headers.get('Connection')?.includes('Upgrade') && req.headers.get('Upgrade')?.includes('websocket')) { if (req.headers.get('Connection')?.includes('Upgrade') && req.headers.get('Upgrade')?.includes('websocket')) {
const isSignalR = url.searchParams.has('id'); const isSignalR = url.searchParams.has('id');
@@ -87,7 +96,11 @@ const server = Deno.serve({ hostname: "10.0.1.39", port: 13370, onListen: addr =
log.w(`No secret set!`); log.w(`No secret set!`);
return unauthRes; return unauthRes;
} }
const payload = (await verify(splitHeader, secret)) as ProfileToken; const payload = JSON.parse(JSON.stringify(await verify(splitHeader, secret))) as TokenFormat;
if (payload.typ !== TokenType.Access) {
log.w(`Only access tokens can be used to connect to the socket`);
return unauthRes;
}
const profile = await Server.Profiles.get(payload.sub); const profile = await Server.Profiles.get(payload.sub);
if (!profile) return new Response("Internal Server Error (profile)", { status: 500 }); if (!profile) return new Response("Internal Server Error (profile)", { status: 500 });
@@ -118,11 +131,11 @@ const server = Deno.serve({ hostname: "10.0.1.39", port: 13370, onListen: addr =
} }
} }
const res = await AppRoot.app.fetch(req, { srcAddr }); const res = await AppRoot.app.fetch(req, { srcAddr });
const netlog = detailedLog([srcAddr, const netlog = detailedLog([srcAddr,
`${res.status}: ${req.method} ${getFullPathFromUrl(new URL(req.url))}`, `${typeof res.status == 'number' ? res.status : "SENT STACK TRACE"}: ${req.method} ${getFullPathFromUrl(new URL(req.url))}`,
formatHeader(req.headers, 'Content-Type'), formatHeader(req.headers, 'Content-Type'),
formatHeader(req.headers, 'Connection'), formatHeader(req.headers, 'Connection'),
formatHeader(req.headers, 'User-Agent'), formatHeader(req.headers, 'User-Agent'),
@@ -148,6 +161,8 @@ Deno.addSignalListener('SIGINT', () => {
for (const socket of consoleSockets) socket.destroy(); for (const socket of consoleSockets) socket.destroy();
for (const socket of gameSockets) socket.sendNotification(PushNotificationId.ModerationQuitGame); for (const socket of gameSockets) socket.sendNotification(PushNotificationId.ModerationQuitGame);
Object.values(Server).forEach(base => ((base as ServerContentBase).destroy ? (base as ServerContentBase).destroy() : undefined));
}); });
Server.Commands.addRootCommand(new Command({ Server.Commands.addRootCommand(new Command({

8
src/net.ts Normal file
View File

@@ -0,0 +1,8 @@
export function getNetConfig() {
return {
host: "10.0.1.39",
port: 13370,
publicHost: "10.0.1.39:13370",
securePublicHost: false
}
}

View File

@@ -1,18 +1,15 @@
import { createHonoRoute } from "../../../util/import.ts"; import { createHonoRoute } from "../../../util/import.ts";
import { authenticate } from "../../../util/api.ts"; import { authenticate, galvanicError, GalvanicErrors, RateLimiter, recNetError } from "../../../util/api.ts";
import Server from "../../../server/server.ts"; import Server from "../../../server/server.ts";
import z from "zod"; import z from "zod";
import { typedZValidator } from "../../../util/validators.ts"; import { transformStringToEnum, typedZValidator } from "../../../util/validators.ts";
import { PlatformType } from "../../../server/platforms/types.ts";
import Steam from "../../../util/steam/steam.ts";
export const route = createHonoRoute('/account'); export const route = createHonoRoute('/account');
const transformNumber = (arg: string, ctx: z.RefinementCtx<string>) => {
const int = parseInt(arg);
if (isNaN(int) || !Number.isSafeInteger(int)) ctx.addIssue('Number is not valid');
else return int;
}
const bulkAccountQuerySchema = z.object({ const bulkAccountQuerySchema = z.object({
id: z.union([ z.string().transform(transformNumber), z.array(z.string().transform(transformNumber)) ]) id: z.union([ z.coerce.number(), z.array(z.coerce.number()) ])
}); });
route.app.get('/bulk', typedZValidator('query', bulkAccountQuerySchema), async c => { route.app.get('/bulk', typedZValidator('query', bulkAccountQuerySchema), async c => {
const { id } = c.req.valid('query'); const { id } = c.req.valid('query');
@@ -29,6 +26,51 @@ route.app.get('/bulk', typedZValidator('query', bulkAccountQuerySchema), async c
); );
}); });
const postCreateRateLimiter = new RateLimiter(60, 3);
const createAccountBodySchema = z.object({
platform: z.string().transform(transformStringToEnum<PlatformType>(PlatformType)),
platformId: z.string().min(14).max(20),
deviceId: z.string().min(32).max(64)
});
route.app.post('/create', postCreateRateLimiter.middle(), typedZValidator('form', createAccountBodySchema), async c => {
const form = c.req.valid('form');
if (typeof form.platform == 'undefined')
return c.json(galvanicError(GalvanicErrors.jex));
else if (form.platform == PlatformType.Steam) {
const steam = await Steam.GetPlayerSummaries([form.platformId]);
if (steam.length == 0)
return c.json(galvanicError(GalvanicErrors.sploot));
const cachedlogins = await Server.Platforms.getCachedLogins(form.platform, form.platformId, true);
if (cachedlogins.length == 0) {
const profile = await Server.Profiles.create(form.platform, form.platformId, steam[0].realname ?? steam[0].personaname);
if (!profile) return c.json(galvanicError(GalvanicErrors.sploot));
Server.Content.steamAvatarDownloadForProfile(profile, steam[0].avatarfull);
return c.json({
success: true,
value: profile.export()
});
} else {
const profile = await Server.Profiles.create(form.platform, form.platformId);
if (!profile) return c.json(galvanicError(GalvanicErrors.sploot));
return c.json({
success: true,
value: profile.export()
});
}
} else return c.json(recNetError("Not a Steam user"));
});
route.app.use(authenticate); route.app.use(authenticate);
route.app.get('/me', c => { route.app.get('/me', c => {

View File

@@ -1,6 +0,0 @@
import { successResponse } from "../../../util/api.ts";
import { createHonoRoute } from "../../../util/import.ts";
export const route = createHonoRoute('/gamesight');
route.app.post('/event', successResponse(true, ""));

View File

@@ -0,0 +1,7 @@
import { createHonoRoute } from "../../../util/import.ts";
export const route = createHonoRoute('/undead');
route.app.get('/v1/emotes', c => {
return c.json([]);
});

View File

@@ -6,7 +6,7 @@ export const route = createHonoRoute("/versioncheck");
const versionCheckSchema = z.object({ const versionCheckSchema = z.object({
v: z.string(), v: z.string(),
p: z.string().transform(Number), p: z.coerce.number(),
}); });
enum VersionStatus { enum VersionStatus {
@@ -15,7 +15,7 @@ enum VersionStatus {
UpdateRequired UpdateRequired
} }
export const gameVerString = '20220118'; export const gameVerString = '20200306';
route.app.get('/v4', typedZValidator('query', versionCheckSchema), c => { route.app.get('/v4', typedZValidator('query', versionCheckSchema), c => {
const { v, p } = c.req.valid('query'); const { v, p } = c.req.valid('query');

View File

@@ -1,7 +1,7 @@
import z from "zod"; import z from "zod";
import { createHonoRoute } from "../../../util/import.ts"; import { createHonoRoute } from "../../../util/import.ts";
import { transformStringToEnum, typedZValidator } from "../../../util/validators.ts"; import { transformStringToEnum, typedZValidator } from "../../../util/validators.ts";
import { PlatformType } from "../../../server/platforms/base.ts"; import { PlatformType } from "../../../server/platforms/types.ts";
import Server from "../../../server/server.ts"; import Server from "../../../server/server.ts";
import { authenticate } from "../../../util/api.ts"; import { authenticate } from "../../../util/api.ts";
import Logging from "@proxnet/undead-logging"; import Logging from "@proxnet/undead-logging";
@@ -28,6 +28,8 @@ const forPlatformIdsReqSchema = z.object({
}); });
route.app.post('/forplatformids', typedZValidator('form', forPlatformIdsReqSchema), async c => { route.app.post('/forplatformids', typedZValidator('form', forPlatformIdsReqSchema), async c => {
const { id } = c.req.valid('form'); const { id } = c.req.valid('form');
log.d(`forplatformids: ${id}`);
const ids = await Server.Platforms.getCachedLogins(PlatformType.Steam, id, true); const ids = await Server.Platforms.getCachedLogins(PlatformType.Steam, id, true);
return c.json(ids || []); return c.json(ids || []);
}); });

View File

@@ -1,7 +1,8 @@
import { createHonoRoute } from "../../../util/import.ts"; import { createHonoRoute } from "../../../util/import.ts";
import z from "zod"; import z from "zod";
import { transformStringToEnum, typedZValidator } from "../../../util/validators.ts"; import { transformStringToEnum, typedZValidator } from "../../../util/validators.ts";
import { DeviceClass, PlatformType, steamAuthTicketSchema } from "../../../server/platforms/base.ts"; import { DeviceClass, PlatformType, TokenFormat, TokenType } from "../../../server/platforms/types.ts";
import { steamAuthTicketSchema } from "../../../server/platforms/base.ts";
import { gameVerString } from "../../api/routes/versioncheck.ts"; import { gameVerString } from "../../api/routes/versioncheck.ts";
import Steam from "../../../util/steam/steam.ts"; import Steam from "../../../util/steam/steam.ts";
import { SteamAuthResult } from "../../../util/steam/SteamAuthTypes.ts"; import { SteamAuthResult } from "../../../util/steam/SteamAuthTypes.ts";
@@ -22,12 +23,9 @@ const authBodyBaseSchema = z.object({
platform_id: z.string().min(4), platform_id: z.string().min(4),
device_id: z.string().min(4), device_id: z.string().min(4),
device_class: z.string().transform(transformStringToEnum<DeviceClass>(DeviceClass)), device_class: z.string().transform(transformStringToEnum<DeviceClass>(DeviceClass)),
time: z.string().transform(Date), time: z.coerce.date(),
ver: z.literal(gameVerString), ver: z.literal(gameVerString),
build_key: z.string().min(4), asid: z.coerce.number(),
asid: z.string().transform(Number),
eac_challenge: z.literal("who said it"),
eac_response: z.literal("who_said_it"),
platform_auth: z.string().transform((arg, ctx) => { platform_auth: z.string().transform((arg, ctx) => {
try { try {
const parsed = steamAuthTicketSchema.safeParse(JSON.parse(arg)) const parsed = steamAuthTicketSchema.safeParse(JSON.parse(arg))
@@ -39,9 +37,6 @@ const authBodyBaseSchema = z.object({
}) })
}); });
const createAccountGrantSchema = authBodyBaseSchema.extend({
grant_type: z.literal("create_account")
});
const cachedLoginGrantSchema = authBodyBaseSchema.extend({ const cachedLoginGrantSchema = authBodyBaseSchema.extend({
grant_type: z.literal('cached_login'), grant_type: z.literal('cached_login'),
account_id: z.string().transform(Number), account_id: z.string().transform(Number),
@@ -52,21 +47,29 @@ const refreshTokenGrantSchema = authBodyBaseSchema.extend({
}); });
const tokenGrantSchema = z.discriminatedUnion('grant_type', [ const tokenGrantSchema = z.discriminatedUnion('grant_type', [
createAccountGrantSchema,
cachedLoginGrantSchema, cachedLoginGrantSchema,
refreshTokenGrantSchema refreshTokenGrantSchema
]); ]);
enum TokenRequestError { enum TokenRequestError {
InvalidRequest = "invalid_request", InvalidRequest = "invalid_request",
InvalidGrant = "invalid_grant",
InvalidClient = "invalid_client", InvalidClient = "invalid_client",
InvalidUsernameOrPassword = "invalid_username_or_password", InvalidGrant = "invalid_grant",
InvalidTime = "invalid time", UnauthorizedClient = "unauthorized_client",
InvalidPlatform = "invalid platform", UnsupportedGrantType = "unsupported_grant_type",
UnsupportedResponseType = "unsupported_response_type",
InvalidScope = "invalid_scope",
AuthorizationPending = "authorization_pending",
AccessDenied = "access_denied", AccessDenied = "access_denied",
SlowDown = "slow_down", SlowDown = "slow_down",
PlatformVerificationFailed = "platform verification failed" ExpiredToken = "expired_token"
}
enum TokenRequestErrorDescriptions {
InvalidUsernameOrPassword = "invalid_username_or_password",
InvalidTime = "invalid time",
PlatformVerificationFailed = "platform verification failed",
InvalidPlatform = "invalid platform",
InvalidDeviceClass = "invalid device class"
} }
route.app.post('/token', typedZValidator('form', tokenGrantSchema), async c => { route.app.post('/token', typedZValidator('form', tokenGrantSchema), async c => {
@@ -75,55 +78,47 @@ route.app.post('/token', typedZValidator('form', tokenGrantSchema), async c => {
} }
const form = c.req.valid('form'); const form = c.req.valid('form');
if (typeof form.platform_auth == 'undefined' || typeof form.platform == 'undefined') return error(TokenRequestError.InvalidPlatform); if (typeof form.platform_auth == 'undefined' || typeof form.platform == 'undefined') return error(TokenRequestError.AccessDenied);
const { valid } = await Steam.AuthenticateUserTicket(form.platform_auth, form.platform_id); const { valid } = await Steam.AuthenticateUserTicket(form.platform_auth, form.platform_id);
if (valid == SteamAuthResult.Failure) return error(TokenRequestError.PlatformVerificationFailed); if (valid == SteamAuthResult.Failure) return error(TokenRequestError.AccessDenied, TokenRequestErrorDescriptions.PlatformVerificationFailed);
if (Math.abs(Date.now() - new Date(form.time).getTime()) > 3_600_000) return error(TokenRequestError.InvalidTime); if (Math.abs(Date.now() - new Date(form.time).getTime()) > 3_600_000) return error(TokenRequestError.AccessDenied, TokenRequestErrorDescriptions.InvalidTime);
const logins = await Server.Platforms.getCachedLogins(form.platform, form.platform_id, false); const logins = await Server.Platforms.getCachedLogins(form.platform, form.platform_id, false);
if (form.grant_type == 'create_account' && logins && logins.length > 0) return error(TokenRequestError.InvalidRequest); if (form.grant_type == 'refresh_token') {
else if (form.grant_type == 'create_account') {
const profile = await Server.Profiles.create();
if (!profile) return error(TokenRequestError.AccessDenied);
await Server.Platforms.addCachedLogin(form.platform, form.platform_id, profile?.getId());
const token = await Server.Platforms.getToken(profile.getId(), await profile.getRole() || 'user');
return c.json({
access_token: token,
refresh_token: token,
key: "aHVo"
});
} else if (form.grant_type == 'refresh_token') {
const secret = Deno.env.get('SECRET'); const secret = Deno.env.get('SECRET');
if (!secret) { if (!secret) {
log.w(`Secret not set!`); log.w(`Secret not set!`);
return error(TokenRequestError.InvalidRequest); return error(TokenRequestError.InvalidRequest);
} }
try { try {
await verify(form.refresh_token, secret); const token = JSON.parse(JSON.stringify(await verify(form.refresh_token, secret))) as TokenFormat;
const profile = await Server.Profiles.get(token.sub);
if (!profile) return error(TokenRequestError.AccessDenied);
const accessToken = await Server.Platforms.getToken(profile.getId(), TokenType.Access);
return c.json({ return c.json({
access_token: form.refresh_token, access_token: accessToken,
refresh_token: form.refresh_token, refresh_token: form.refresh_token,
key: "aHVo"
}); });
} catch (err) { } catch (err) {
log.w(`Authentication error (token req): ${(err as Error).stack}`); log.w(`Authentication error (token req): ${(err as Error).stack}`);
return error(TokenRequestError.InvalidRequest); return error(TokenRequestError.InvalidClient);
} }
} }
if (logins && logins.find(login => login.accountId === form.account_id)) { if (logins.find(login => login.accountId === form.account_id)) {
const profile = await Server.Profiles.get(form.account_id); const profile = await Server.Profiles.get(form.account_id);
if (!profile) return error(TokenRequestError.InvalidRequest, "No such profile"); if (!profile) return error(TokenRequestError.InvalidRequest, "No such profile");
await Server.Platforms.updateLastLoginTime(form.platform, form.platform_id, form.account_id); await Server.Platforms.updateLastLoginTime(form.platform, form.platform_id, form.account_id);
const token = await Server.Platforms.getToken(profile.getId(), await profile.getRole() || 'user'); const accessToken = await Server.Platforms.getToken(profile.getId(), TokenType.Access);
const refreshToken = await Server.Platforms.getToken(profile.getId(), TokenType.Refresh);
return c.json({ return c.json({
access_token: token, access_token: accessToken,
refresh_token: token, refresh_token: refreshToken,
key: "aHVo"
}); });
} else return error(TokenRequestError.InvalidRequest, "No such profile"); } else return error(TokenRequestError.InvalidRequest, "No such profile");
}); });

View File

@@ -1,7 +0,0 @@
import { createHonoRoute } from "../../../util/import.ts";
export const route = createHonoRoute("/eac");
route.app.get('/challenge', c => {
return c.text(`"who said it"`);
});

View File

@@ -5,6 +5,6 @@ export const route = createHonoRoute("/player");
route.app.use(authenticate); route.app.use(authenticate);
route.app.post('/login', async c => { route.app.post('/login', _c => {
return c.status(200); return new Response("OK", { status: 200 });
}); });

View File

@@ -1,31 +1,22 @@
import { getNetConfig } from "../../net.ts";
import { createHonoRoute } from "../../util/import.ts"; import { createHonoRoute } from "../../util/import.ts";
export const route = createHonoRoute('/'); export const route = createHonoRoute('/');
const netConfig = getNetConfig();
route.app.get('/', async (c, next) => { route.app.get('/', async (c, next) => {
if (c.req.query('v') == '2') return c.json({ if (c.req.query('v') === '2') return c.json({
Accounts: "https://wsi.proxnet.dev/accounts", Auth: `${netConfig.securePublicHost ? 'https' : 'http'}://${netConfig.publicHost}/auth`,
API: "https://wsi.proxnet.dev/", API: `${netConfig.securePublicHost ? 'https' : 'http'}://${netConfig.publicHost}/`,
Auth: "https://wsi.proxnet.dev/auth", Notifications: `${netConfig.securePublicHost ? 'https' : 'http'}://${netConfig.publicHost}/notify`,
BugReporting: "https://wsi.proxnet.dev/bugs", Images: `${netConfig.securePublicHost ? 'https' : 'http'}://${netConfig.publicHost}/img`,
CDN: "https://wsi.proxnet.dev/cdn", CDN: `${netConfig.securePublicHost ? 'https' : 'http'}://${netConfig.publicHost}/cdn`,
Chat: "https://wsi.proxnet.dev/chat", Commerce: `${netConfig.securePublicHost ? 'https' : 'http'}://${netConfig.publicHost}/commerce`,
Clubs: "https://wsi.proxnet.dev/clubs", Matchmaking: `${netConfig.securePublicHost ? 'https' : 'http'}://${netConfig.publicHost}/match`,
Commerce: "https://wsi.proxnet.dev/commerce", Storage: `${netConfig.securePublicHost ? 'https' : 'http'}://${netConfig.publicHost}/storage`,
DataCollection: "https://wsi.proxnet.dev/datacol", Chat: `${netConfig.securePublicHost ? 'https' : 'http'}://${netConfig.publicHost}/chat`,
Discovery: "https://wsi.proxnet.dev/disc", Leaderboard: `${netConfig.securePublicHost ? 'https' : 'http'}://${netConfig.publicHost}/leaderboard`,
Images: "https://wsi.proxnet.dev/img", Accounts: `${netConfig.securePublicHost ? 'https' : 'http'}://${netConfig.publicHost}/accounts`,
Leaderboard: "https://wsi.proxnet.dev/leaderboard",
Link: "https://wsi.proxnet.dev/link",
Matchmaking: "https://wsi.proxnet.dev/match",
Moderation: "https://wsi.proxnet.dev/mod",
Notifications: "https://wsi.proxnet.dev/notify",
PlatformNotifications: "https://wsi.proxnet.dev/platnotify",
PlayerSettings: "https://wsi.proxnet.dev/plsettings",
RoomComments: "https://wsi.proxnet.dev/roomcomments",
Rooms: "https://wsi.proxnet.dev/rooms",
Storage: "https://wsi.proxnet.dev/storage",
WWW: "https://wsi.proxnet.dev/www",
}); });
return await next(); return await next();
}); });

View File

@@ -15,7 +15,7 @@ export class ServerContentBase {
} }
/** /**
* Event method - ran when server starts (listens on address) * Event method - ran before server starts
* *
* Override me! * Override me!
*/ */
@@ -23,4 +23,13 @@ export class ServerContentBase {
return; return;
} }
/**
* Event method - ran before server stops
*
* Override me!
*/
destroy() {
return;
}
} }

View File

@@ -1,6 +1,7 @@
import path from "node:path"; import path from "node:path";
import { ServerContentBase } from "../ContentBase.ts"; import { ServerContentBase } from "../ContentBase.ts";
import { RootPath } from "../../util/path.ts"; import { RootPath } from "../../util/path.ts";
import { PlatformMask } from "../platforms/types.ts";
interface AvatarImport { interface AvatarImport {
allPossibleCombinations: { allPossibleCombinations: {
@@ -20,6 +21,7 @@ interface AvatarItemExport {
AvatarItemType: AvatarItemType, AvatarItemType: AvatarItemType,
AvatarItemDesc: string, AvatarItemDesc: string,
FriendlyName: string, FriendlyName: string,
PlatformMask: number,
Tooltip: string, Tooltip: string,
Rarity: ItemRarity Rarity: ItemRarity
} }
@@ -61,7 +63,8 @@ export class AvatarContentBase extends ServerContentBase {
AvatarItemType: AvatarItemType.Outfit, AvatarItemType: AvatarItemType.Outfit,
AvatarItemDesc: formatVisualData(data._avatarItemVisualData), AvatarItemDesc: formatVisualData(data._avatarItemVisualData),
FriendlyName: data._avatarItemData.Name, FriendlyName: data._avatarItemData.Name,
Tooltip: "pre-avatar update item", PlatformMask: PlatformMask.All,
Tooltip: "Galvanic Avatar Item",
Rarity: ItemRarity.None Rarity: ItemRarity.None
})); }));
} }

View File

@@ -1,19 +0,0 @@
enum _OutfitType {
None = -1,
Hat,
Hair = 2,
Ear,
Eye = 10,
Beard = 20,
Shoulder = 100,
Shirt,
Waist,
Neck,
TeamJersey,
Wrist = 200,
TeamWrist = 203
}
// figure out the order in which ids go into AvatarItemDesc
// then create a function in `base.ts` (next to this file) that can turn enums corresponding to avatar items into a full AvatarItemDesc string
// - that may require codegen. i can probably do it though. i know you can.

181
src/server/content/base.ts Normal file
View File

@@ -0,0 +1,181 @@
import Logging from "@proxnet/undead-logging";
import { ServerContentBase } from "../ContentBase.ts";
import type Profile from "../profiles/profile.ts";
import { generateRandomString } from "../../util/api.ts";
export enum FileType {
Unknown,
RoomSave,
Holotar,
Image,
Video,
Invention
}
interface RawMetaFile {
Type: FileType,
CreatedAt: string,
SavedBy?: number
OriginalFilename?: string
}
interface MetaFile {
Type: FileType,
CreatedAt: Date,
SavedBy?: Profile
OriginalFilename?: string
}
interface File {
Meta: MetaFile,
Data: Uint8Array<ArrayBufferLike>
}
interface FileCreationResult {
success: boolean
}
interface FileCreationSuccess extends FileCreationResult {
success: true,
newFilename: string
}
interface FileCreationFailure extends FileCreationResult {
success: false,
error: Error
}
type FileCreation = FileCreationSuccess | FileCreationFailure;
export class ServerContentManager extends ServerContentBase {
#log = new Logging("ServerContent");
override start() {
Array.fromAsync(Deno.readDir('./persist')).then(entries => {
if (!entries.find(entry => entry.isDirectory && entry.name == 'user')) {
this.#log.i(`Creating user folders`);
this.#createUserFolders();
}
});
}
async #createUserFolders() {
await Deno.mkdir('./persist/user');
await Deno.mkdir('./persist/user/room');
await Deno.mkdir('./persist/user/holotar');
await Deno.mkdir('./persist/user/img');
await Deno.mkdir('./persist/user/video');
await Deno.mkdir('./persist/user/invention');
}
async steamAvatarDownloadForProfile(prof: Profile, steamUrl: string) {
await fetch(steamUrl).then(async res => {
const url = new URL(res.url);
const split = url.pathname.split('/');
const filename = split[split.length - 1];
this.saveFile(await res.bytes(), FileType.Image, filename).then(res => {
if (res.success == true) {
prof.setProfileImg(res.newFilename);
this.#log.i(`Saved profile image from Steam for profile ${prof.getId()}: "${res.newFilename}"`);
}
else this.#log.w(`Could not save profile image from Steam for profile ${prof.getId()}: ${res.error}`);
});
}).catch(reas => {
this.#log.w(`Could not fetch steam URL and download for profile: ${reas}`);
});
}
async getAvailabileFileName(ext: string, prefix: string) {
let filename = generateRandomString(18);
while ((await Array.fromAsync(Deno.readDir(`./persist/user/${prefix ? prefix : ""}`)))
.find(entry => entry.isFile && entry.name == `${filename}.${ext}`)) filename = await this.getAvailabileFileName(ext, prefix);
return filename;
}
fileTypeToExt(type: FileType) {
switch (type) {
case FileType.RoomSave:
return "room";
case FileType.Holotar:
return "holo";
case FileType.Image:
return "img";
case FileType.Video:
return "vid";
case FileType.Invention:
return "inv";
default:
return "blob";
}
}
async saveFile(data: Uint8Array<ArrayBufferLike>, type: FileType, filename?: string, prof?: Profile): Promise<FileCreation> {
let targetFolder = "";
switch (type) {
case FileType.RoomSave:
targetFolder = "room/";
break;
case FileType.Holotar:
targetFolder = "holotar/";
break;
case FileType.Image:
targetFolder = "img/";
break;
case FileType.Video:
targetFolder = "video/";
break;
case FileType.Invention:
targetFolder = "invention/";
break;
}
const ext = this.fileTypeToExt(type);
const newFilename = await this.getAvailabileFileName(ext, targetFolder);
try {
await Deno.writeFile(`./persist/user/${targetFolder}${newFilename}.${ext}`, data);
const metaRaw: RawMetaFile = {
Type: type,
CreatedAt: new Date().toISOString(),
SavedBy: prof?.getId(),
OriginalFilename: filename
}
await Deno.writeTextFile(`./persist/user/${targetFolder}${newFilename}.${ext}.meta`, JSON.stringify(metaRaw));
const success: FileCreationSuccess = {
success: true,
newFilename: `${newFilename}.${ext}`
}
return success;
} catch (err) {
this.#log.w(`Could not save file (typ: ${FileType}, name: ${filename}, prof: ${prof ? prof.getId() : undefined}): ${err}`);
const error: FileCreationFailure = {
success: false,
error: (err as Error)
}
return error;
}
}
async getFile(path: string) {
try {
const data = await Deno.readFile(`./persist/user/${path}`);
const meta = await Deno.readTextFile(`./persist/user/${path}.meta`);
const metaRaw: RawMetaFile = JSON.parse(meta);
const metaParsed: MetaFile = {
Type: metaRaw.Type,
CreatedAt: new Date(metaRaw.CreatedAt),
SavedBy: metaRaw.SavedBy ? (await this.server.Profiles.get(metaRaw.SavedBy) ?? undefined) : undefined,
OriginalFilename: metaRaw.OriginalFilename
}
const file: File = {
Meta: metaParsed,
Data: data
}
return file;
} catch (err) {
this.#log.w(`Could not get file "${path}": ${err}`);
return null;
}
}
}

View File

@@ -0,0 +1,39 @@
import type Profile from "../profiles/profile.ts";
import { RoomLocation } from "./base.ts";
export class Instance {
#createdAt = new Date();
#players: Set<Profile> = new Set();
#instanceId: number;
#location: RoomLocation;
constructor(options: {
id: number,
location: RoomLocation,
}
) {
this.#instanceId = options.id;
this.#location = options.location;
}
getPlayers() {
return this.#players.values().toArray();
}
playerIsHere(profile: Profile) {
return this.getPlayers().find(prof => prof.same(profile)) ? true : false;
}
removePlayer(profile: Profile) {
this.#players.delete(profile);
}
addPlayer(profile: Profile) {
this.#players.add(profile);
}
getCreatedAt() {
return this.#createdAt;
}
}

View File

@@ -0,0 +1,63 @@
import { ServerContentBase } from "../ContentBase.ts";
export enum RoomLocation {
Calibration = "f5fbd9c9-e853-4036-9d48-5f68e861af04",
DormRoom = "76d98498-60a1-430c-ab76-b54a29b7a163",
RecCenter = "cbad71af-0831-44d8-b8ef-69edafa841f6",
Charades = "4078dfed-24bb-4db7-863f-578ba48d726b",
TheInkSpace = "1fa06e3c-c307-4c11-a91b-1fabcddb8a96",
Paddleball = "d89f74fa-d51e-477a-a425-025a891dd499",
GoldenTrophy = "91e16e35-f48f-4700-ab8a-a1b79e50e51b",
Orientation = "c79709d8-a31b-48aa-9eb8-cc31ba9505e8",
TheRiseofJumbotron = "acc06e66-c2d0-4361-b0cd-46246a4c455c",
CrimsonCauldron = "949fa41f-4347-45c0-b7ac-489129174045",
IsleOfLostSkulls = "7e01cfe0-820a-406f-b1b3-0a5bf575235c",
RecRoyaleSquads = "253fa009-6e65-4c90-91a1-7137a56a267f",
RecRoyaleSolos = "b010171f-4875-4e89-baba-61e878cd41e1",
Lounge = "a067557f-ca32-43e6-b6e5-daaec60b4f5a",
PerformanceHall = "9932f88f-3929-43a0-a012-a40b5128e346",
MakerRoom = "a75f7547-79eb-47c6-8986-6767abcb4f92",
Park = "0a864c86-5a71-4e18-8041-8124e4dc9d98",
Lake = "f6f7256c-e438-4299-b99e-d20bef8cf7e0",
PropulsionTestRange = "d9378c9f-80bc-46fb-ad1e-1bed8a674f55",
Gym = "3d474b26-26f7-45e9-9a36-9b02847d5e6f",
Stadium = "6d5eea4b-f069-4ed0-9916-0e2f07df0d03",
Hangar = "239e676c-f12f-489f-bf3a-d4c383d692c3",
CyberJunkCity = "9d6456ce-6264-48b4-808d-2d96b3d91038",
Crescendo = "49cb8993-a956-43e2-86f4-1318f279b22a",
BowlingAlley = "ae929543-9a07-41d5-8ee9-dbbee8c36800",
AnimationRecordingStudio = "a95c349c-0f96-4c2d-a4c8-4969ffa8ea44",
StuntRunner = "b7281665-a715-4051-826b-8e08e69c6172",
TheMainEvent = "3a636bd2-f896-424c-9225-c184522c0d87",
StuntRunnerBaseRoom = "882e9b96-7115-4b03-86f6-c0c9d8e22e00",
Registration = "cf61556d-68fd-4288-9ae5-7a512621e569",
ARRoom = "bf268f5f-b55b-41af-8628-32fa4b5d70b6",
PaintballRiver = "e122fe98-e7db-49e8-a1b1-105424b6e1f0",
PaintballHomestead = "a785267d-c579-42ea-be43-fec1992d1ca7",
PaintballQuarry = "ff4c6427-7079-4f59-b22a-69b089420827",
PaintballClearcut = "380d18b5-de9c-49f3-80f7-f4a95c1de161",
PaintballSpillway = "58763055-2dfb-4814-80b8-16fac5c85709",
PaintballDriveIn = "65ddbb48-5a01-4e3e-972d-e5c7419e2bc3",
}
export interface RoomInstance {
roomInstanceId: number,
roomId: number,
subRoomId: number,
location: RoomLocation,
name: string,
maxCapacity: number,
isFull: boolean,
isPrivate: boolean,
isInProgress: boolean,
photonRegionId: string,
photonRoomId: string,
dataBlob?: string,
eventId?: string
}
export class InstanceManager extends ServerContentBase {
}

View File

@@ -3,56 +3,13 @@ import Command from "../commands/command.ts";
import { ServerContentBase } from "../ContentBase.ts"; import { ServerContentBase } from "../ContentBase.ts";
import { transformStringToEnum } from "../../util/validators.ts"; import { transformStringToEnum } from "../../util/validators.ts";
import { sign } from "@hono/hono/jwt"; import { sign } from "@hono/hono/jwt";
import { CachedLogin, DbCachedLogin, PlatformMask, PlatformType, TokenFormat, TokenType } from "./types.ts";
export enum PlatformType {
All = -1,
Steam,
Oculus,
PlayStation,
Xbox,
WindowsPlatformless,
IOS,
GooglePlay
}
export enum DeviceClass {
Unknown,
VR,
Screen,
Mobile,
VRLow,
Quest2
}
interface DbCachedLogin {
accountId: number,
lastLoginTime: Date,
requirePassword: boolean
}
export interface CachedLogin extends DbCachedLogin {
platformId: string
platform: PlatformType
}
export const steamAuthTicketSchema = z.object({ export const steamAuthTicketSchema = z.object({
Ticket: z.string().min(256), Ticket: z.string().min(256),
AppId: z.literal("471710") AppId: z.literal("471710")
}); });
export enum ProfileRole {
Developer = "developer",
Moderator = "moderator",
Screenshare = "screenshare",
User = "user"
}
interface TokenFormat {
iss: string,
exp: number,
sub: number,
role: string
}
export class PlatformsManager extends ServerContentBase { export class PlatformsManager extends ServerContentBase {
static platformsKey = "platforms"; static platformsKey = "platforms";
@@ -61,15 +18,15 @@ export class PlatformsManager extends ServerContentBase {
return [PlatformsManager.platformsKey, ...keys.filter(val => typeof val == 'string')]; return [PlatformsManager.platformsKey, ...keys.filter(val => typeof val == 'string')];
} }
async getToken(accountId: number, role: string) { async getToken(accountId: number, type: TokenType) {
const secret = Deno.env.get('SECRET'); const secret = Deno.env.get('SECRET');
if (!secret) throw new Error("No SECRET in env. Did you forget to set it?"); if (!secret) throw new Error("No SECRET in env. Did you forget to set it?");
const token: TokenFormat = { const token: TokenFormat = {
typ: type,
sub: accountId, sub: accountId,
role, iss: "https://yarns.proxnet.dev/auth/",
iss: "https://wsi.proxnet.dev/auth/", exp: type == TokenType.Access ? Math.round(Date.now() / 1000) + 21_600 : Math.round(Date.now() / 1000) + 31_556_952
exp: Math.round(Date.now() / 1000) + 21_600
} }
return await sign(JSON.parse(JSON.stringify(token)), secret); return await sign(JSON.parse(JSON.stringify(token)), secret);
} }
@@ -106,8 +63,8 @@ export class PlatformsManager extends ServerContentBase {
}; };
} }
async getCachedLogins(platform: PlatformType, platformId: string, format: true): Promise<CachedLogin[] | null> async getCachedLogins(platform: PlatformType, platformId: string, format: true): Promise<CachedLogin[]>
async getCachedLogins(platform: PlatformType, platformId: string, format: false): Promise<DbCachedLogin[] | null> async getCachedLogins(platform: PlatformType, platformId: string, format: false): Promise<DbCachedLogin[]>
async getCachedLogins(platform: PlatformType, platformId: string, format?: boolean) { async getCachedLogins(platform: PlatformType, platformId: string, format?: boolean) {
const set = await this.kv.getKv().get<Set<DbCachedLogin>>(this.#constructPlatformKey(platform, platformId)); const set = await this.kv.getKv().get<Set<DbCachedLogin>>(this.#constructPlatformKey(platform, platformId));
if (set.value && format) return set.value.values().toArray().map(val => ({ if (set.value && format) return set.value.values().toArray().map(val => ({
@@ -118,7 +75,7 @@ export class PlatformsManager extends ServerContentBase {
requirePassword: val.requirePassword requirePassword: val.requirePassword
} as CachedLogin)); } as CachedLogin));
else if (set.value) return set.value.values().toArray(); else if (set.value) return set.value.values().toArray();
else return null; else return [];
} }
async deleteCachedLogin(platform: PlatformType, platformId: string, accountId: number) { async deleteCachedLogin(platform: PlatformType, platformId: string, accountId: number) {
@@ -136,6 +93,30 @@ export class PlatformsManager extends ServerContentBase {
} else return null; } else return null;
} }
getPlatformMask(value: number) {
const err = new Error("Invalid mask");
if (typeof value !== 'number' || !Number.isInteger(value)) throw err;
if (value === PlatformMask.All) {
return [PlatformMask.All];
}
return Object.values(PlatformMask)
.filter(v => typeof v === "number" && v !== PlatformMask.None && v !== PlatformMask.All)
.filter(v => (value & (v as number)) === v) as PlatformMask[];
}
buildPlatformMask(...flags: PlatformMask[]) {
const err = new Error("Invalid mask");
if (!flags.length) throw err;
for (const flag of flags)
if (typeof flag !== 'number' || !Object.values(PlatformMask).includes(flag))
throw err;
return flags.reduce((mask, flag) => mask | flag, 0);
}
override start() { override start() {
this.server.Commands.addRootCommand(new Command({ this.server.Commands.addRootCommand(new Command({
key: ['platforms', 'pm', 'platformmanager', 'platformanager'], key: ['platforms', 'pm', 'platformmanager', 'platformanager'],

View File

@@ -0,0 +1,59 @@
export enum TokenType {
Access,
Refresh
}
export interface TokenFormatBase {
typ: TokenType
}
export interface TokenFormat extends TokenFormatBase {
iss: string,
exp: number,
sub: number,
}
export enum ProfileRole {
Developer = 'developer',
Moderator = 'moderator',
Web = 'webClient',
Game = 'gameClient'
}
export enum PlatformType {
All = -1,
Steam,
Oculus,
PlayStation,
Microsoft,
HeadlessBot,
IOS,
}
export enum PlatformMask {
None = 0,
Steam = 1,
Oculus = 2,
PlayStation = 4,
Microsoft = 8,
HeadlessBot = 16,
IOS = 32,
All = -1
}
export enum DeviceClass {
Unknown,
VR,
Screen,
Mobile,
VRLow
}
export interface DbCachedLogin {
accountId: number,
lastLoginTime: Date,
requirePassword: boolean
}
export interface CachedLogin extends DbCachedLogin {
platformId: string
platform: PlatformType
}

View File

@@ -1,16 +1,132 @@
import Logging from "@proxnet/undead-logging";
import { ServerContentBase } from "../ContentBase.ts"; import { ServerContentBase } from "../ContentBase.ts";
import type Profile from "../profiles/profile.ts"; import { DeviceClass } from "../platforms/types.ts";
import Profile from "../profiles/profile.ts";
import { type ServerBase } from "../server.ts";
import { RoomInstance } from "../instances/base.ts";
class Presence { export enum VRMovementMode {
TELEPORT,
WALK
}
export enum PlayerStatusVisibility {
Public,
FriendsOnly,
FavoriteFriendsOnly,
Offline
}
export interface PresenceExport {
playerId: number,
statusVisibility: PlayerStatusVisibility,
deviceClass: DeviceClass,
vrMovementMode?: VRMovementMode,
roomInstance: RoomInstance | null
}
export class Presence {
#server: ServerBase;
#profile: Profile;
#statusVisibility: PlayerStatusVisibility = PlayerStatusVisibility.Offline;
#deviceClass: DeviceClass = DeviceClass.Unknown;
#roomInstance: RoomInstance | null = null;
#vrMovementMove: VRMovementMode | undefined;
#lastExported: Date = new Date();
constructor(profile: Profile, server: ServerBase) {
this.#profile = profile;
this.#server = server;
}
/** Refer to `Profile.Matchmaking.updateLastSeen` */
updateLastSeen() {
this.#profile.Matchmaking.updateLastSeen();
}
async update() {
this.#deviceClass = (await this.#profile.Matchmaking.getLastDeviceClass()) || DeviceClass.Unknown;
const isOnline = (Date.now() - (await this.#profile.Matchmaking.getLastSeen()).getTime()) < 90_000;
this.#statusVisibility =
isOnline ?
this.#statusVisibility :
PlayerStatusVisibility.Offline;
this.#roomInstance = this.#profile.getInstance();
this.#server.emit('presence.update', { profile: this.#profile, presence: this });
}
setStatusVisibility(sv: PlayerStatusVisibility) {
this.#statusVisibility = sv;
this.updateLastSeen();
this.update();
}
setVRMovementMode(mm: VRMovementMode) {
this.#vrMovementMove = mm;
this.updateLastSeen();
this.update();
}
getLastExported() {
return this.#lastExported;
}
logout() {
this.updateLastSeen();
this.#statusVisibility = PlayerStatusVisibility.Offline;
}
export() {
this.#lastExported = new Date();
const e: PresenceExport = {
playerId: this.#profile.getId(),
statusVisibility: this.#statusVisibility,
deviceClass: this.#deviceClass,
vrMovementMode: this.#vrMovementMove,
roomInstance: this.#roomInstance
}
return e;
}
} }
export class PresenceBase extends ServerContentBase { export class PresenceBase extends ServerContentBase {
#log = new Logging("Presence");
#presenceMap: Map<Profile, Presence> = new Map(); #presenceMap: Map<Profile, Presence> = new Map();
getPresence() { #intervalId?: number;
#deleteDeadPresences() {
for (const pres of this.#presenceMap.values()) {
if (Date.now() - pres.getLastExported().getTime() > 300_000) pres
}
}
getPresence(profile: Profile) {
let pres = this.#presenceMap.get(profile);
if (!pres) {
pres = new Presence(profile, this.server);
this.#presenceMap.set(profile, pres);
}
return pres;
}
override start() {
this.#intervalId = setInterval(() => {
this.#log.i('Clearing dead presences');
this.#deleteDeadPresences();
}, 300_000);
}
override destroy() {
clearInterval(this.#intervalId ?? undefined);
} }
} }

View File

@@ -0,0 +1,7 @@
import type Profile from "../../profiles/profile.ts";
import { type Presence } from "../base.ts";
export interface PresenceUpdateEvent {
presence: Presence,
profile: Profile
}

View File

@@ -1,23 +1,14 @@
import { DeviceClass } from "../../platforms/types.ts";
import ProfileContentManager from "./base.ts"; import ProfileContentManager from "./base.ts";
export enum DeviceClass {
Unknown,
VR,
Screen,
Mobile,
VRLow,
Quest2
}
export class ProfileMatchmakingManager extends ProfileContentManager { export class ProfileMatchmakingManager extends ProfileContentManager {
#deviceClassKey = this.profile.constructProfilePropertyKey('deviceclass'); #deviceClassKey = this.profile.constructProfilePropertyKey('deviceclass');
async setLastDeviceClass(dc: DeviceClass) { async setLastDeviceClass(dc: DeviceClass) {
await this.kv.getKv().set(this.#deviceClassKey, dc); await this.kv.getKv().set(this.#deviceClassKey, dc);
} }
async getLastDeviceClass(): Promise<DeviceClass | null> { async getLastDeviceClass(): Promise<DeviceClass> {
return (await this.kv.getKv().get<DeviceClass>(this.#deviceClassKey)).value || null; return (await this.kv.getKv().get<DeviceClass>(this.#deviceClassKey)).value || DeviceClass.Unknown;
} }
#loginLockKey = this.profile.constructProfilePropertyKey('loginlock'); #loginLockKey = this.profile.constructProfilePropertyKey('loginlock');
@@ -31,4 +22,14 @@ export class ProfileMatchmakingManager extends ProfileContentManager {
return (await this.kv.getKv().get<string>(this.#loginLockKey)).value ? true : false; return (await this.kv.getKv().get<string>(this.#loginLockKey)).value ? true : false;
} }
#lastSeen = this.profile.constructProfilePropertyKey('lastseen');
async updateLastSeen() {
await this.kv.getKv().set(this.#lastSeen, new Date());
}
async getLastSeen() {
const value = await this.kv.getKv().get<Date>(this.#lastSeen);
if (value.value) return value.value;
else return this.profile.getCreationDate();
}
} }

View File

@@ -8,6 +8,7 @@ class ProfileContentManager {
constructor(profile: Profile, kv: KV) { constructor(profile: Profile, kv: KV) {
this.profile = profile; this.profile = profile;
this.kv = kv; this.kv = kv;
profile.managers.push(this);
} }
} }

View File

@@ -1,4 +1,4 @@
import { type ProfileRole } from "../../platforms/base.ts"; import { type ProfileRole } from "../../platforms/types.ts";
import type Profile from "../profile.ts"; import type Profile from "../profile.ts";
export interface RoleUpdateEvent { export interface RoleUpdateEvent {

View File

@@ -4,7 +4,7 @@ import Profile from "./profile.ts";
import { SelfAccount, type RecNetAccount } from "./types/profile.ts"; import { SelfAccount, type RecNetAccount } from "./types/profile.ts";
import Command from "./../commands/command.ts"; import Command from "./../commands/command.ts";
import z from "zod"; import z from "zod";
import { ProfileRole } from "../platforms/base.ts"; import { PlatformMask, PlatformType, ProfileRole } from "../platforms/types.ts";
const profiles: Map<number, Profile> = new Map(); const profiles: Map<number, Profile> = new Map();
@@ -13,15 +13,15 @@ class ProfileManagerBase extends ServerContentBase {
static profilesKey = "profiles"; static profilesKey = "profiles";
static profileByNameKey = "profileName"; static profileByNameKey = "profileName";
/*async exists(id: number) {
return (await this.kv.getKv().get([ ProfileManagerBase.profilesKey, id ])).value !== null
}*/
async #getUnusedId() { async #getUnusedId() {
let id = Math.round(Math.random() * 2_147_483_647); let id = Math.round(Math.random() * 2_147_483_647);
if (await this.get(id)) id = await this.#getUnusedId(); if (await this.get(id)) id = await this.#getUnusedId();
return id; return id;
} }
getActiveProfileReferences() {
return profiles.values().toArray();
}
async #getUnusedUsername() { async #getUnusedUsername() {
const adjective = NameDictionary.Adjectives[Math.floor(Math.random() * NameDictionary.Adjectives.length)]; const adjective = NameDictionary.Adjectives[Math.floor(Math.random() * NameDictionary.Adjectives.length)];
@@ -31,20 +31,28 @@ class ProfileManagerBase extends ServerContentBase {
if (await this.getByUsername(username)) username = await this.#getUnusedUsername(); if (await this.getByUsername(username)) username = await this.#getUnusedUsername();
return username; return username;
} }
async #getUsernameDefault(username: string) {
const prof = await this.getByUsername(username);
if (!prof) return username;
else return await this.#getUnusedUsername();
}
async create(username?: string) { async create(platform: PlatformType, platformId: string, username?: string) {
const id = await this.#getUnusedId(); const id = await this.#getUnusedId();
const newUsername = username? username : await this.#getUnusedUsername(); const newUsername = username ? await this.#getUsernameDefault(username) : await this.#getUnusedUsername();
const newProfile: RecNetAccount = { const newProfile: RecNetAccount = {
accountId: id, accountId: id,
username: newUsername, username: newUsername,
displayName: newUsername, displayName: newUsername,
platforms: PlatformMask.None,
profileImage: "DefaultProfileImage.png", profileImage: "DefaultProfileImage.png",
createdAt: new Date() createdAt: new Date()
} }
await this.kv.getKv().set([ ProfileManagerBase.profilesKey, id ], newProfile); await this.kv.getKv().set([ ProfileManagerBase.profilesKey, id ], newProfile);
await this.kv.getKv().set([ ProfileManagerBase.profilesKey, newUsername ], id); await this.kv.getKv().set([ ProfileManagerBase.profilesKey, newUsername ], id);
await this.server.Platforms.addCachedLogin(platform, platformId, id);
return this.get(id); return this.get(id);
} }

View File

@@ -1,8 +1,11 @@
import { RoomInstance } from "../instances/base.ts";
import KV from "../persistence/kv.ts"; import KV from "../persistence/kv.ts";
import { ProfileRole } from "../platforms/base.ts"; import { PlatformMask, ProfileRole } from "../platforms/types.ts";
import { type ServerBase } from "../server.ts"; import { type ServerBase } from "../server.ts";
import { type SignalRSocketHandler } from "../socket/signalr/socket.ts"; import { type SignalRSocketHandler } from "../socket/signalr/socket.ts";
import { ProfileAvatarManager } from "./content/Avatar.ts"; import { ProfileAvatarManager } from "./content/Avatar.ts";
import ProfileContentManager from "./content/base.ts";
import { ProfileMatchmakingManager } from "./content/Matchmaking.ts";
import { ProfileSettingsManager } from "./content/Settings.ts"; import { ProfileSettingsManager } from "./content/Settings.ts";
import ProfileManagerBase from "./manager.ts"; import ProfileManagerBase from "./manager.ts";
import { recNetAccountSchema, SelfAccount, type RecNetAccount } from "./types/profile.ts"; import { recNetAccountSchema, SelfAccount, type RecNetAccount } from "./types/profile.ts";
@@ -13,12 +16,16 @@ class Profile {
#kv: KV; #kv: KV;
#socket: SignalRSocketHandler | null = null; #socket: SignalRSocketHandler | null = null;
#server: ServerBase; #instance: RoomInstance | null = null;
#server: ServerBase;
#selfAcc: SelfAccount; #selfAcc: SelfAccount;
managers: ProfileContentManager[] = [];
Settings: ProfileSettingsManager; Settings: ProfileSettingsManager;
Avatar: ProfileAvatarManager; Avatar: ProfileAvatarManager;
Matchmaking: ProfileMatchmakingManager;
constructor(acc: SelfAccount, kv: KV, server: ServerBase) { constructor(acc: SelfAccount, kv: KV, server: ServerBase) {
this.#id = acc.accountId; this.#id = acc.accountId;
@@ -28,6 +35,7 @@ class Profile {
this.Settings = new ProfileSettingsManager(this, this.#kv); this.Settings = new ProfileSettingsManager(this, this.#kv);
this.Avatar = new ProfileAvatarManager(this, this.#kv); this.Avatar = new ProfileAvatarManager(this, this.#kv);
this.Matchmaking = new ProfileMatchmakingManager(this, this.#kv);
} }
async #saveSelfAcc() { async #saveSelfAcc() {
@@ -67,11 +75,12 @@ class Profile {
async setBio(bio: string) { async setBio(bio: string) {
const key = this.constructProfilePropertyKey('bio'); const key = this.constructProfilePropertyKey('bio');
await this.#kv.getKv().set(key, bio); await this.#kv.getKv().set(key, bio);
this.#server.emit('profile.update', { profile: this });
} }
async getRole(): Promise<ProfileRole> { async getRole(): Promise<ProfileRole> {
const val = (await this.#kv.getKv().get<ProfileRole>(this.constructProfilePropertyKey('role'))).value; const val = (await this.#kv.getKv().get<ProfileRole>(this.constructProfilePropertyKey('role'))).value;
if (!val) return ProfileRole.User; if (!val) return ProfileRole.Game;
else return val; else return val;
} }
@@ -80,6 +89,42 @@ class Profile {
this.#server.emit('profile.roleupdate', { profile: this, newRole: role }); this.#server.emit('profile.roleupdate', { profile: this, newRole: role });
} }
async addPlatform(type: PlatformMask) {
const platforms = this.#server.Platforms.getPlatformMask(this.#selfAcc.platforms);
this.#selfAcc.platforms = this.#server.Platforms.buildPlatformMask(...[...platforms, type]);
await this.#saveSelfAcc();
}
async removePlatform(type: PlatformMask) {
const platforms = new Set(this.#server.Platforms.getPlatformMask(this.#selfAcc.platforms));
platforms.delete(type);
this.#selfAcc.platforms = this.#server.Platforms.buildPlatformMask(...platforms.values().toArray());
await this.#saveSelfAcc();
}
async setProfileImg(img: string) {
this.#selfAcc.profileImage = img;
await this.#saveSelfAcc();
}
getProfileImg() {
return this.#selfAcc.profileImage;
}
getCreationDate() {
return this.#selfAcc.createdAt;
}
getPlatforms() {
return this.#selfAcc.platforms;
}
getInstance() {
return this.#instance;
}
setInstance(inst: RoomInstance) {
this.#instance = inst;
}
getId() { getId() {
return this.#id; return this.#id;
} }
@@ -102,6 +147,10 @@ class Profile {
return this.#selfAcc; return this.#selfAcc;
} }
same(profile: Profile) {
return profile.getId() == this.getId();
}
} }
export default Profile; export default Profile;

View File

@@ -1,12 +1,21 @@
import z from "zod"; import z from "zod";
import { ProfileRole } from "../../platforms/base.ts"; import { PlatformMask } from "../../platforms/types.ts";
import Server from "../../server.ts";
export const recNetAccountSchema = z.object({ export const recNetAccountSchema = z.object({
accountId: z.number(), accountId: z.number(),
profileImage: z.string(), profileImage: z.string(),
isJunior: z.optional(z.boolean()), isJunior: z.coerce.boolean().optional(),
username: z.string(), username: z.string(),
displayName: z.string(), displayName: z.string(),
platforms: z.number().transform(arg => {
try {
Server.Platforms.getPlatformMask(arg);
return arg;
} catch {
return PlatformMask.All;
}
}),
createdAt: z.union([ z.date(), z.string().transform((arg, ctx) => { createdAt: z.union([ z.date(), z.string().transform((arg, ctx) => {
const d = new Date(arg); const d = new Date(arg);
if (isNaN(d.getTime())) { if (isNaN(d.getTime())) {
@@ -24,14 +33,4 @@ export const selfAccountSchema = recNetAccountSchema.extend({
}); });
export type RecNetAccount = z.infer<typeof recNetAccountSchema>; export type RecNetAccount = z.infer<typeof recNetAccountSchema>;
export type SelfAccount = z.infer<typeof selfAccountSchema>; export type SelfAccount = z.infer<typeof selfAccountSchema>;
export const profileTokenSchema = z.object({
iss: z.string(),
exp: z.number().min(Date.now()),
iat: z.number().min(Date.now()),
sub: z.number(),
role: z.enum(ProfileRole)
});
export type ProfileToken = z.infer<typeof profileTokenSchema>;

View File

@@ -1,15 +1,19 @@
import { AvatarContentBase } from "./avatars/base.ts"; import { AvatarContentBase } from "./avatars/base.ts";
import { EventManager } from "./baseevent.ts"; import { EventManager } from "./baseevent.ts";
import { CommandsBase } from "./commands/commands.ts"; import { CommandsBase } from "./commands/commands.ts";
import { ServerContentManager } from "./content/base.ts";
import GameConfigsBase from "./gameconfigs/base.ts"; import GameConfigsBase from "./gameconfigs/base.ts";
import { InstanceManager } from "./instances/base.ts";
import { PlatformsManager } from "./platforms/base.ts"; import { PlatformsManager } from "./platforms/base.ts";
import { type PresenceUpdateEvent } from "./presence/events/PresenceUpdateEvent.ts";
import { type ProfileUpdateEvent } from "./profiles/events/ProfileUpdate.ts"; import { type ProfileUpdateEvent } from "./profiles/events/ProfileUpdate.ts";
import { type RoleUpdateEvent } from "./profiles/events/RoleUpdate.ts"; import { type RoleUpdateEvent } from "./profiles/events/RoleUpdate.ts";
import ProfileManagerBase from "./profiles/manager.ts"; import ProfileManagerBase from "./profiles/manager.ts";
interface ServerEvents { interface ServerEvents {
'profile.roleupdate': RoleUpdateEvent, 'profile.roleupdate': RoleUpdateEvent,
'profile.update': ProfileUpdateEvent 'profile.update': ProfileUpdateEvent,
'presence.update': PresenceUpdateEvent,
} }
class ServerBase extends EventManager<ServerEvents> { class ServerBase extends EventManager<ServerEvents> {
@@ -18,6 +22,8 @@ class ServerBase extends EventManager<ServerEvents> {
Commands = new CommandsBase(this, 'commands'); Commands = new CommandsBase(this, 'commands');
Platforms = new PlatformsManager(this, 'platforms', true); Platforms = new PlatformsManager(this, 'platforms', true);
Avatars = new AvatarContentBase(this, 'avatars'); Avatars = new AvatarContentBase(this, 'avatars');
Instances = new InstanceManager(this, 'instances');
Content = new ServerContentManager(this, "content");
} }
const Server = new ServerBase(); const Server = new ServerBase();

View File

@@ -72,7 +72,7 @@ export class SignalRSocketHandler {
this.sendRaw({}); this.sendRaw({});
return; return;
} else { } else {
if (logmessages) this.#log.d(`CLIENT MESSAGE\n Type: ${message.data.type} (${SignalMessageType[message.data.type]})\n ${JSON.stringify(message.data)}`); if (logmessages) this.#log.d(`CLIENT MESSAGE\n Type: ${message.data.type} (${SignalMessageType[message.data.type]})\n Content: ${JSON.stringify(message.data)}`);
if (message.data.type == SignalMessageType.Invocation && message.data.invocationId) { // don't send completion messages for nonblocking invocations if (message.data.type == SignalMessageType.Invocation && message.data.invocationId) { // don't send completion messages for nonblocking invocations
const res = await this.#dispatchTarget(message.data.target, message.data.arguments[0]); // rec room only uses the first index const res = await this.#dispatchTarget(message.data.target, message.data.arguments[0]); // rec room only uses the first index
if (res.type == TargetResultType.Success) { if (res.type == TargetResultType.Success) {

View File

@@ -5,7 +5,9 @@ export class PlayerSocketSubscriptionTarget extends SocketTarget {
#ids: number[] = []; #ids: number[] = [];
override zod = z.tuple([]).rest(z.number()); override zod = z.object({
PlayerIds: z.array(z.number().nonnegative().max(2_147_483_647))
});
override exec(...ids: number[]) { override exec(...ids: number[]) {
this.#ids = ids; this.#ids = ids;

View File

@@ -5,7 +5,7 @@ export class SocketTarget {
socket: SignalRSocketHandler; socket: SignalRSocketHandler;
zod: z.ZodTuple = z.tuple([]); zod: z.ZodObject = z.object({});
constructor(socket: SignalRSocketHandler) { constructor(socket: SignalRSocketHandler) {
this.socket = socket; this.socket = socket;

View File

@@ -150,14 +150,13 @@ export enum PushNotificationId {
RelationshipChanged = 1, RelationshipChanged = 1,
MessageReceived, MessageReceived,
MessageDeleted, MessageDeleted,
PresenceHeartbeatResponse, PresenceHeartbeatResponse, // unused by the game
RefreshLogin, RefreshLogin,
Logout, Logout,
SubscriptionUpdateProfile = 11, SubscriptionUpdateProfile = 11,
SubscriptionUpdatePresence, SubscriptionUpdatePresence,
SubscriptionUpdateGameSession, SubscriptionUpdateGameSession,
SubscriptionUpdateRoom = 15, SubscriptionUpdateRoom = 15,
SubscriptionUpdateRoomPlaylist,
ModerationQuitGame = 20, ModerationQuitGame = 20,
ModerationUpdateRequired, ModerationUpdateRequired,
ModerationKick, ModerationKick,
@@ -166,7 +165,6 @@ export enum PushNotificationId {
ServerMaintenance, ServerMaintenance,
GiftPackageReceived = 30, GiftPackageReceived = 30,
GiftPackageReceivedImmediate, GiftPackageReceivedImmediate,
GiftPackageRewardSelectionReceived,
ProfileJuniorStatusUpdate = 40, ProfileJuniorStatusUpdate = 40,
RelationshipsInvalid = 50, RelationshipsInvalid = 50,
StorefrontBalanceAdd = 60, StorefrontBalanceAdd = 60,
@@ -184,7 +182,4 @@ export enum PushNotificationId {
CommunityBoardUpdate = 95, CommunityBoardUpdate = 95,
CommunityBoardAnnouncementUpdate, CommunityBoardAnnouncementUpdate,
InventionModerationStateChanged = 100, InventionModerationStateChanged = 100,
FreeGiftButtonItemsAdded = 110,
LocalRoomKeyCreated = 120,
LocalRoomKeyDeleted
} }

View File

@@ -4,7 +4,7 @@ import Logging from "@proxnet/undead-logging";
import z from "zod"; import z from "zod";
import { verify } from "@hono/hono/jwt"; import { verify } from "@hono/hono/jwt";
import Server from "../server/server.ts"; import Server from "../server/server.ts";
import { ProfileToken } from "../server/profiles/types/profile.ts"; import { TokenFormat } from "../server/platforms/types.ts";
const log = new Logging("APIUtils"); const log = new Logging("APIUtils");
@@ -34,8 +34,8 @@ export async function authenticate(c: Context<HonoEnv>, nxt: Next) {
if (authHeader.success) { if (authHeader.success) {
try { try {
const payload = await verify(authHeader.data ? authHeader.data : 'not a valid token', secret); const payload = JSON.parse(JSON.stringify(await verify(authHeader.data ? authHeader.data : 'not a valid token', secret)));
const profile = await Server.Profiles.get((payload as ProfileToken).sub); const profile = await Server.Profiles.get((payload as TokenFormat).sub);
if (!profile) return c.json(genericResponse(false, "Internal Server Error"), 500); if (!profile) return c.json(genericResponse(false, "Internal Server Error"), 500);
c.set('profile', profile); c.set('profile', profile);
@@ -46,4 +46,86 @@ export async function authenticate(c: Context<HonoEnv>, nxt: Next) {
} }
} else return c.json(genericResponse(false, "Authorization required"), 401); } else return c.json(genericResponse(false, "Authorization required"), 401);
}
export enum GalvanicErrors {
jex = "jex", // Error in account creation, check platform
sploot = "sploot", // Error in account creation, steamid was not valid or profile could not be created
}
export function galvanicError(code: GalvanicErrors) {
return {success: false, error:`Galvanic Error (code: ${code})`};
}
export function recNetError(err: string) {
return {success:false, error: err};
}
export function generateRandomString(length: number) {
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let randomString = "";
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * characters.length);
randomString += characters.charAt(randomIndex);
}
return randomString;
}
export class RateLimiter {
#intervalId: number;
#hitLimit: number;
#addressHits: Map<string, number> = new Map();
/**
* @param interval In seconds: rate at which hit counts will be cleared
* @param limit Number of hits (inclusive) before requests are blocked
*/
constructor(interval: number = 60, limit: number = 10) {
this.#hitLimit = limit;
this.#intervalId = setInterval(() => {
this.#addressHits.clear();
}, interval * 1000);
Deno.addSignalListener("SIGINT", () => {
this.#close();
});
}
#addressIncrement(address: string) {
const hits = this.#addressHits.get(address);
if (hits) this.#addressHits.set(address, hits + 1);
else this.#addressHits.set(address, 1);
}
#getAddressHits(address: string) {
const hits = this.#addressHits.get(address);
if (hits) return hits;
else {
this.#addressHits.set(address, 1);
return 1;
}
}
middle() {
return (
c: Context<HonoEnv>,
next: Next
) => {
const address = c.get('srcAddr');
if (address == '127.0.0.1' || address == '::1') return next();
this.#addressIncrement(address);
const hits = this.#getAddressHits(address);
if (hits && hits > this.#hitLimit) return c.json(recNetError("Rate Limited. Please try again later."), 429);
else return next();
};
}
#close() {
clearInterval(this.#intervalId);
}
} }

View File

@@ -1,6 +1,3 @@
import { Context } from "@hono/hono";
import { getConnInfo } from "@hono/hono/deno";
export function getSourceAddress(req: Request, netAddr?: Deno.NetAddr) { export function getSourceAddress(req: Request, netAddr?: Deno.NetAddr) {
let addr = '(unknown src)'; let addr = '(unknown src)';
@@ -14,20 +11,7 @@ export function getSourceAddress(req: Request, netAddr?: Deno.NetAddr) {
if (first) addr = first; if (first) addr = first;
return addr; return addr;
} }
export function getHonoSourceAddress(c: Context) {
let addr = '(unknown src)';
const { remote } = getConnInfo(c);
const sources = [
c.header('Cf-Connecting-Ip'),
c.header('X-Real-Ip'),
remote.address ? remote.port ? `${remote.address}:${remote.port}` : remote.address : null
];
const first = sources.find(val => val !== null);
if (first) addr = first;
return addr;
}
export function getFullPathFromUrl(url: URL) { export function getFullPathFromUrl(url: URL) {
const params = url.searchParams.toString(); const params = url.searchParams.toString();
return `${url.pathname}${params ? `?${params}` : ''}`; return `${url.pathname}${params ? `?${params}` : ''}`;

15
src/util/photon.ts Normal file
View File

@@ -0,0 +1,15 @@
export enum CloudRegionCode {
eu = "eu",
us = "us",
asia = "asia",
jp = "jp",
au = "au",
usw = "usw",
sa = "sa",
cae = "cae",
kr = "kr",
in = "in",
ru = "ru",
rue = "rue",
none = "none"
}

View File

@@ -20,6 +20,7 @@ import type { PersonaState, CommunityVisibilityState } from "./steam.ts";
export interface SteamPlayer { export interface SteamPlayer {
steamid: string; steamid: string;
personaname: string; personaname: string;
realname?: string;
profileurl: string; profileurl: string;
avatar: string; avatar: string;
avatarmedium: string; avatarmedium: string;

View File

@@ -45,7 +45,7 @@ export enum CommunityVisibilityState {
class SteamBase { class SteamBase {
async GetPlayerSummaries(steamids: string[]) { async GetPlayerSummaries(steamids: string[]) {
if (!steamkey) return null; if (!steamkey) return [];
const params = new URLSearchParams(); const params = new URLSearchParams();
params.append('key', steamkey); params.append('key', steamkey);
@@ -53,13 +53,13 @@ class SteamBase {
try { try {
const res = await fetch(`${buildSteamUrl('ISteamUser', 'GetPlayerSummaries/v2')}?${params}`); const res = await fetch(`${buildSteamUrl('ISteamUser', 'GetPlayerSummaries/v2')}?${params}`);
if (res.status !== 200) return null; if (res.status !== 200) return [];
const resjson = await res.json() as { response: { players: SteamPlayer[] } }; const resjson = await res.json() as { response: { players: SteamPlayer[] } };
return resjson.response.players; return resjson.response.players;
} catch (err) { } catch (err) {
log.e(`Could not fetch Steam player summaries: ${(err as Error).stack}`); log.e(`Could not fetch Steam player summaries: ${(err as Error).stack}`);
return null; return [];
} }
} }