galvanic corrosion rewrite
commit this before something goes horribly wrong
This commit is contained in:
99044
res/avatar.json
99044
res/avatar.json
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@
|
||||
"m_Enabled": 1,
|
||||
"m_Script": {
|
||||
"m_FileID": 1,
|
||||
"m_PathID": 5244
|
||||
"m_PathID": 3147
|
||||
},
|
||||
"m_Name": "EquipmentWardrobeRuntimeConfig",
|
||||
"toolSkinMaps": [
|
||||
@@ -75,14 +75,6 @@
|
||||
{
|
||||
"skinAssetName": "PaintballGun_Skin_Pirate",
|
||||
"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",
|
||||
"skinGuid": "ZMAx_-B_7kWy6-TLXx8g6Q"
|
||||
},
|
||||
{
|
||||
"skinAssetName": "PaintballShotgun_Skin_Caution",
|
||||
"skinGuid": "CwIBKjm3G0-xUGt_9Gf7UQ"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -143,10 +131,6 @@
|
||||
{
|
||||
"skinAssetName": "Basketball_Skin_Cozy",
|
||||
"skinGuid": "WOWwmg7jg0mH2g4LJUp6fA"
|
||||
},
|
||||
{
|
||||
"skinAssetName": "Basketball_Skin_Gold",
|
||||
"skinGuid": "bNEcMJOzokeEVKDeXk_cDQ"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -175,14 +159,6 @@
|
||||
{
|
||||
"skinAssetName": "PaintballShield_Skin_Gingerbread",
|
||||
"skinGuid": "QTyRLpDB3UuReHQqrKxb5A"
|
||||
},
|
||||
{
|
||||
"skinAssetName": "PaintballShield_Plaid_Skin",
|
||||
"skinGuid": "gfGhbEMx_kCRdO-6vPOh3g"
|
||||
},
|
||||
{
|
||||
"skinAssetName": "PaintballShield_Skin_Caution",
|
||||
"skinGuid": "ClJApV0LC0mxh0UiYhaXGQ"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -223,22 +199,6 @@
|
||||
{
|
||||
"skinAssetName": "QuestShield_Skin_Pride",
|
||||
"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",
|
||||
"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",
|
||||
"skinGuid": "dEmh0tniCkeYBY1EdD09jA"
|
||||
},
|
||||
{
|
||||
"skinAssetName": "Quest_SciFi_AutomaticGun_Plaid_Skin",
|
||||
"skinGuid": "jzgz_HrPAUqOeqx3uNXVUA"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -331,18 +279,6 @@
|
||||
{
|
||||
"skinAssetName": "Quest_SciFi_Shotgun_Skin_Valentine",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"skinGuid": "70uy5UJhhEy1aynKM4MAsQ"
|
||||
},
|
||||
{
|
||||
"skinAssetName": "RecRoyale_Backpack_Gold_Skin",
|
||||
"skinGuid": "ujZ4Hl0HO06OysqZcqHhmg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -911,14 +635,6 @@
|
||||
{
|
||||
"skinAssetName": "ShareCamera_Skin_Heart",
|
||||
"skinGuid": "QQALCzCF-0ClDWqmmciHAQ"
|
||||
},
|
||||
{
|
||||
"skinAssetName": "ShareCamera_Skin_Caution",
|
||||
"skinGuid": "SkTa53OzM0u82YPuZAl9aw"
|
||||
},
|
||||
{
|
||||
"skinAssetName": "ShareCamera_Skin_Plaid",
|
||||
"skinGuid": "sE4_-ZVa2EC4MZfGGsExzQ"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1099,82 +815,6 @@
|
||||
{
|
||||
"skinAssetName": "PaintballAssaultRifle_Skin_wood",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
836
res/rooms.json
836
res/rooms.json
File diff suppressed because it is too large
Load Diff
39
src/main.ts
39
src/main.ts
@@ -7,17 +7,23 @@ import Command from "./server/commands/command.ts";
|
||||
import { ServerContentBase } from "./server/ContentBase.ts";
|
||||
import z from "zod";
|
||||
import { verify } from "@hono/hono/jwt";
|
||||
import { type ProfileToken } from "./server/profiles/types/profile.ts";
|
||||
import { SignalRSocketHandler } from "./server/socket/signalr/socket.ts";
|
||||
import { PushNotificationId } from "./server/socket/signalr/types.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.resetLogTiming = LogTiming.Microtask;
|
||||
|
||||
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)[]) {
|
||||
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));
|
||||
}
|
||||
|
||||
const server = Deno.serve({ hostname: "10.0.1.39", port: 13370, onListen: addr => {
|
||||
log.n(`Listening info: ${JSON.stringify(addr)}`);
|
||||
const netConfig = getNetConfig();
|
||||
const server = Deno.serve({
|
||||
hostname: netConfig.host, port: netConfig.port, onListen: addr => {
|
||||
log.n(`Listening info: ${JSON.stringify(addr)}`);
|
||||
|
||||
onListen();
|
||||
}}, async (req, info) => {
|
||||
onListen();
|
||||
}
|
||||
}, async (req, info) => {
|
||||
|
||||
const url = new URL(req.url);
|
||||
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') {
|
||||
return new Response(JSON.stringify({
|
||||
connectionId: "who_said_it",
|
||||
availableTransports: [{transport:"WebSockets",transferFormats:["Text"]}]
|
||||
}), { headers: { 'Content-Type': 'application/json' }});
|
||||
connectionId: "galv4",
|
||||
availableTransports: [{ transport: "WebSockets", transferFormats: ["Text"] }]
|
||||
}), { headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
if (req.headers.get('Connection')?.includes('Upgrade') && req.headers.get('Upgrade')?.includes('websocket')) {
|
||||
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!`);
|
||||
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);
|
||||
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 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, 'Connection'),
|
||||
formatHeader(req.headers, 'User-Agent'),
|
||||
@@ -148,6 +161,8 @@ Deno.addSignalListener('SIGINT', () => {
|
||||
|
||||
for (const socket of consoleSockets) socket.destroy();
|
||||
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({
|
||||
|
||||
8
src/net.ts
Normal file
8
src/net.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export function getNetConfig() {
|
||||
return {
|
||||
host: "10.0.1.39",
|
||||
port: 13370,
|
||||
publicHost: "10.0.1.39:13370",
|
||||
securePublicHost: false
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,15 @@
|
||||
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 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');
|
||||
|
||||
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({
|
||||
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 => {
|
||||
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.get('/me', c => {
|
||||
|
||||
@@ -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, ""));
|
||||
7
src/routes/api/routes/undead.ts
Normal file
7
src/routes/api/routes/undead.ts
Normal 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([]);
|
||||
});
|
||||
@@ -6,7 +6,7 @@ export const route = createHonoRoute("/versioncheck");
|
||||
|
||||
const versionCheckSchema = z.object({
|
||||
v: z.string(),
|
||||
p: z.string().transform(Number),
|
||||
p: z.coerce.number(),
|
||||
});
|
||||
|
||||
enum VersionStatus {
|
||||
@@ -15,7 +15,7 @@ enum VersionStatus {
|
||||
UpdateRequired
|
||||
}
|
||||
|
||||
export const gameVerString = '20220118';
|
||||
export const gameVerString = '20200306';
|
||||
|
||||
route.app.get('/v4', typedZValidator('query', versionCheckSchema), c => {
|
||||
const { v, p } = c.req.valid('query');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import z from "zod";
|
||||
import { createHonoRoute } from "../../../util/import.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 { authenticate } from "../../../util/api.ts";
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
@@ -28,6 +28,8 @@ const forPlatformIdsReqSchema = z.object({
|
||||
});
|
||||
route.app.post('/forplatformids', typedZValidator('form', forPlatformIdsReqSchema), async c => {
|
||||
const { id } = c.req.valid('form');
|
||||
log.d(`forplatformids: ${id}`);
|
||||
|
||||
const ids = await Server.Platforms.getCachedLogins(PlatformType.Steam, id, true);
|
||||
return c.json(ids || []);
|
||||
});
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createHonoRoute } from "../../../util/import.ts";
|
||||
import z from "zod";
|
||||
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 Steam from "../../../util/steam/steam.ts";
|
||||
import { SteamAuthResult } from "../../../util/steam/SteamAuthTypes.ts";
|
||||
@@ -22,12 +23,9 @@ const authBodyBaseSchema = z.object({
|
||||
platform_id: z.string().min(4),
|
||||
device_id: z.string().min(4),
|
||||
device_class: z.string().transform(transformStringToEnum<DeviceClass>(DeviceClass)),
|
||||
time: z.string().transform(Date),
|
||||
time: z.coerce.date(),
|
||||
ver: z.literal(gameVerString),
|
||||
build_key: z.string().min(4),
|
||||
asid: z.string().transform(Number),
|
||||
eac_challenge: z.literal("who said it"),
|
||||
eac_response: z.literal("who_said_it"),
|
||||
asid: z.coerce.number(),
|
||||
platform_auth: z.string().transform((arg, ctx) => {
|
||||
try {
|
||||
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({
|
||||
grant_type: z.literal('cached_login'),
|
||||
account_id: z.string().transform(Number),
|
||||
@@ -52,21 +47,29 @@ const refreshTokenGrantSchema = authBodyBaseSchema.extend({
|
||||
});
|
||||
|
||||
const tokenGrantSchema = z.discriminatedUnion('grant_type', [
|
||||
createAccountGrantSchema,
|
||||
cachedLoginGrantSchema,
|
||||
refreshTokenGrantSchema
|
||||
]);
|
||||
|
||||
enum TokenRequestError {
|
||||
InvalidRequest = "invalid_request",
|
||||
InvalidGrant = "invalid_grant",
|
||||
InvalidClient = "invalid_client",
|
||||
InvalidUsernameOrPassword = "invalid_username_or_password",
|
||||
InvalidTime = "invalid time",
|
||||
InvalidPlatform = "invalid platform",
|
||||
InvalidGrant = "invalid_grant",
|
||||
UnauthorizedClient = "unauthorized_client",
|
||||
UnsupportedGrantType = "unsupported_grant_type",
|
||||
UnsupportedResponseType = "unsupported_response_type",
|
||||
InvalidScope = "invalid_scope",
|
||||
AuthorizationPending = "authorization_pending",
|
||||
AccessDenied = "access_denied",
|
||||
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 => {
|
||||
@@ -75,55 +78,47 @@ route.app.post('/token', typedZValidator('form', tokenGrantSchema), async c => {
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
if (form.grant_type == 'create_account' && logins && logins.length > 0) return error(TokenRequestError.InvalidRequest);
|
||||
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') {
|
||||
if (form.grant_type == 'refresh_token') {
|
||||
const secret = Deno.env.get('SECRET');
|
||||
if (!secret) {
|
||||
log.w(`Secret not set!`);
|
||||
return error(TokenRequestError.InvalidRequest);
|
||||
}
|
||||
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({
|
||||
access_token: form.refresh_token,
|
||||
access_token: accessToken,
|
||||
refresh_token: form.refresh_token,
|
||||
key: "aHVo"
|
||||
});
|
||||
} catch (err) {
|
||||
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);
|
||||
if (!profile) return error(TokenRequestError.InvalidRequest, "No such profile");
|
||||
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({
|
||||
access_token: token,
|
||||
refresh_token: token,
|
||||
key: "aHVo"
|
||||
access_token: accessToken,
|
||||
refresh_token: refreshToken,
|
||||
});
|
||||
} else return error(TokenRequestError.InvalidRequest, "No such profile");
|
||||
});
|
||||
@@ -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"`);
|
||||
});
|
||||
@@ -5,6 +5,6 @@ export const route = createHonoRoute("/player");
|
||||
|
||||
route.app.use(authenticate);
|
||||
|
||||
route.app.post('/login', async c => {
|
||||
return c.status(200);
|
||||
route.app.post('/login', _c => {
|
||||
return new Response("OK", { status: 200 });
|
||||
});
|
||||
@@ -1,31 +1,22 @@
|
||||
import { getNetConfig } from "../../net.ts";
|
||||
import { createHonoRoute } from "../../util/import.ts";
|
||||
|
||||
export const route = createHonoRoute('/');
|
||||
|
||||
const netConfig = getNetConfig();
|
||||
route.app.get('/', async (c, next) => {
|
||||
if (c.req.query('v') == '2') return c.json({
|
||||
Accounts: "https://wsi.proxnet.dev/accounts",
|
||||
API: "https://wsi.proxnet.dev/",
|
||||
Auth: "https://wsi.proxnet.dev/auth",
|
||||
BugReporting: "https://wsi.proxnet.dev/bugs",
|
||||
CDN: "https://wsi.proxnet.dev/cdn",
|
||||
Chat: "https://wsi.proxnet.dev/chat",
|
||||
Clubs: "https://wsi.proxnet.dev/clubs",
|
||||
Commerce: "https://wsi.proxnet.dev/commerce",
|
||||
DataCollection: "https://wsi.proxnet.dev/datacol",
|
||||
Discovery: "https://wsi.proxnet.dev/disc",
|
||||
Images: "https://wsi.proxnet.dev/img",
|
||||
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",
|
||||
if (c.req.query('v') === '2') return c.json({
|
||||
Auth: `${netConfig.securePublicHost ? 'https' : 'http'}://${netConfig.publicHost}/auth`,
|
||||
API: `${netConfig.securePublicHost ? 'https' : 'http'}://${netConfig.publicHost}/`,
|
||||
Notifications: `${netConfig.securePublicHost ? 'https' : 'http'}://${netConfig.publicHost}/notify`,
|
||||
Images: `${netConfig.securePublicHost ? 'https' : 'http'}://${netConfig.publicHost}/img`,
|
||||
CDN: `${netConfig.securePublicHost ? 'https' : 'http'}://${netConfig.publicHost}/cdn`,
|
||||
Commerce: `${netConfig.securePublicHost ? 'https' : 'http'}://${netConfig.publicHost}/commerce`,
|
||||
Matchmaking: `${netConfig.securePublicHost ? 'https' : 'http'}://${netConfig.publicHost}/match`,
|
||||
Storage: `${netConfig.securePublicHost ? 'https' : 'http'}://${netConfig.publicHost}/storage`,
|
||||
Chat: `${netConfig.securePublicHost ? 'https' : 'http'}://${netConfig.publicHost}/chat`,
|
||||
Leaderboard: `${netConfig.securePublicHost ? 'https' : 'http'}://${netConfig.publicHost}/leaderboard`,
|
||||
Accounts: `${netConfig.securePublicHost ? 'https' : 'http'}://${netConfig.publicHost}/accounts`,
|
||||
});
|
||||
return await next();
|
||||
});
|
||||
@@ -15,7 +15,7 @@ export class ServerContentBase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Event method - ran when server starts (listens on address)
|
||||
* Event method - ran before server starts
|
||||
*
|
||||
* Override me!
|
||||
*/
|
||||
@@ -23,4 +23,13 @@ export class ServerContentBase {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event method - ran before server stops
|
||||
*
|
||||
* Override me!
|
||||
*/
|
||||
destroy() {
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import path from "node:path";
|
||||
import { ServerContentBase } from "../ContentBase.ts";
|
||||
import { RootPath } from "../../util/path.ts";
|
||||
import { PlatformMask } from "../platforms/types.ts";
|
||||
|
||||
interface AvatarImport {
|
||||
allPossibleCombinations: {
|
||||
@@ -20,6 +21,7 @@ interface AvatarItemExport {
|
||||
AvatarItemType: AvatarItemType,
|
||||
AvatarItemDesc: string,
|
||||
FriendlyName: string,
|
||||
PlatformMask: number,
|
||||
Tooltip: string,
|
||||
Rarity: ItemRarity
|
||||
}
|
||||
@@ -61,7 +63,8 @@ export class AvatarContentBase extends ServerContentBase {
|
||||
AvatarItemType: AvatarItemType.Outfit,
|
||||
AvatarItemDesc: formatVisualData(data._avatarItemVisualData),
|
||||
FriendlyName: data._avatarItemData.Name,
|
||||
Tooltip: "pre-avatar update item",
|
||||
PlatformMask: PlatformMask.All,
|
||||
Tooltip: "Galvanic Avatar Item",
|
||||
Rarity: ItemRarity.None
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -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
181
src/server/content/base.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
39
src/server/instances/Instance.ts
Normal file
39
src/server/instances/Instance.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
63
src/server/instances/base.ts
Normal file
63
src/server/instances/base.ts
Normal 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 {
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -3,56 +3,13 @@ import Command from "../commands/command.ts";
|
||||
import { ServerContentBase } from "../ContentBase.ts";
|
||||
import { transformStringToEnum } from "../../util/validators.ts";
|
||||
import { sign } from "@hono/hono/jwt";
|
||||
|
||||
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
|
||||
}
|
||||
import { CachedLogin, DbCachedLogin, PlatformMask, PlatformType, TokenFormat, TokenType } from "./types.ts";
|
||||
|
||||
export const steamAuthTicketSchema = z.object({
|
||||
Ticket: z.string().min(256),
|
||||
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 {
|
||||
|
||||
static platformsKey = "platforms";
|
||||
@@ -61,15 +18,15 @@ export class PlatformsManager extends ServerContentBase {
|
||||
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');
|
||||
if (!secret) throw new Error("No SECRET in env. Did you forget to set it?");
|
||||
|
||||
const token: TokenFormat = {
|
||||
typ: type,
|
||||
sub: accountId,
|
||||
role,
|
||||
iss: "https://wsi.proxnet.dev/auth/",
|
||||
exp: Math.round(Date.now() / 1000) + 21_600
|
||||
iss: "https://yarns.proxnet.dev/auth/",
|
||||
exp: type == TokenType.Access ? Math.round(Date.now() / 1000) + 21_600 : Math.round(Date.now() / 1000) + 31_556_952
|
||||
}
|
||||
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: false): Promise<DbCachedLogin[] | null>
|
||||
async getCachedLogins(platform: PlatformType, platformId: string, format: true): Promise<CachedLogin[]>
|
||||
async getCachedLogins(platform: PlatformType, platformId: string, format: false): Promise<DbCachedLogin[]>
|
||||
async getCachedLogins(platform: PlatformType, platformId: string, format?: boolean) {
|
||||
const set = await this.kv.getKv().get<Set<DbCachedLogin>>(this.#constructPlatformKey(platform, platformId));
|
||||
if (set.value && format) return set.value.values().toArray().map(val => ({
|
||||
@@ -118,7 +75,7 @@ export class PlatformsManager extends ServerContentBase {
|
||||
requirePassword: val.requirePassword
|
||||
} as CachedLogin));
|
||||
else if (set.value) return set.value.values().toArray();
|
||||
else return null;
|
||||
else return [];
|
||||
}
|
||||
|
||||
async deleteCachedLogin(platform: PlatformType, platformId: string, accountId: number) {
|
||||
@@ -136,6 +93,30 @@ export class PlatformsManager extends ServerContentBase {
|
||||
} 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() {
|
||||
this.server.Commands.addRootCommand(new Command({
|
||||
key: ['platforms', 'pm', 'platformmanager', 'platformanager'],
|
||||
|
||||
59
src/server/platforms/types.ts
Normal file
59
src/server/platforms/types.ts
Normal 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
|
||||
}
|
||||
@@ -1,16 +1,132 @@
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
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 {
|
||||
|
||||
#log = new Logging("Presence");
|
||||
|
||||
#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);
|
||||
}
|
||||
|
||||
}
|
||||
7
src/server/presence/events/PresenceUpdateEvent.ts
Normal file
7
src/server/presence/events/PresenceUpdateEvent.ts
Normal 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
|
||||
}
|
||||
@@ -1,23 +1,14 @@
|
||||
import { DeviceClass } from "../../platforms/types.ts";
|
||||
import ProfileContentManager from "./base.ts";
|
||||
|
||||
export enum DeviceClass {
|
||||
Unknown,
|
||||
VR,
|
||||
Screen,
|
||||
Mobile,
|
||||
VRLow,
|
||||
Quest2
|
||||
}
|
||||
|
||||
|
||||
export class ProfileMatchmakingManager extends ProfileContentManager {
|
||||
|
||||
#deviceClassKey = this.profile.constructProfilePropertyKey('deviceclass');
|
||||
async setLastDeviceClass(dc: DeviceClass) {
|
||||
await this.kv.getKv().set(this.#deviceClassKey, dc);
|
||||
}
|
||||
async getLastDeviceClass(): Promise<DeviceClass | null> {
|
||||
return (await this.kv.getKv().get<DeviceClass>(this.#deviceClassKey)).value || null;
|
||||
async getLastDeviceClass(): Promise<DeviceClass> {
|
||||
return (await this.kv.getKv().get<DeviceClass>(this.#deviceClassKey)).value || DeviceClass.Unknown;
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
#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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,6 +8,7 @@ class ProfileContentManager {
|
||||
constructor(profile: Profile, kv: KV) {
|
||||
this.profile = profile;
|
||||
this.kv = kv;
|
||||
profile.managers.push(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type ProfileRole } from "../../platforms/base.ts";
|
||||
import { type ProfileRole } from "../../platforms/types.ts";
|
||||
import type Profile from "../profile.ts";
|
||||
|
||||
export interface RoleUpdateEvent {
|
||||
|
||||
@@ -4,7 +4,7 @@ import Profile from "./profile.ts";
|
||||
import { SelfAccount, type RecNetAccount } from "./types/profile.ts";
|
||||
import Command from "./../commands/command.ts";
|
||||
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();
|
||||
|
||||
@@ -13,15 +13,15 @@ class ProfileManagerBase extends ServerContentBase {
|
||||
static profilesKey = "profiles";
|
||||
static profileByNameKey = "profileName";
|
||||
|
||||
/*async exists(id: number) {
|
||||
return (await this.kv.getKv().get([ ProfileManagerBase.profilesKey, id ])).value !== null
|
||||
}*/
|
||||
|
||||
async #getUnusedId() {
|
||||
let id = Math.round(Math.random() * 2_147_483_647);
|
||||
if (await this.get(id)) id = await this.#getUnusedId();
|
||||
return id;
|
||||
}
|
||||
|
||||
getActiveProfileReferences() {
|
||||
return profiles.values().toArray();
|
||||
}
|
||||
|
||||
async #getUnusedUsername() {
|
||||
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();
|
||||
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 newUsername = username? username : await this.#getUnusedUsername();
|
||||
const newUsername = username ? await this.#getUsernameDefault(username) : await this.#getUnusedUsername();
|
||||
const newProfile: RecNetAccount = {
|
||||
accountId: id,
|
||||
username: newUsername,
|
||||
displayName: newUsername,
|
||||
platforms: PlatformMask.None,
|
||||
profileImage: "DefaultProfileImage.png",
|
||||
createdAt: new Date()
|
||||
}
|
||||
await this.kv.getKv().set([ ProfileManagerBase.profilesKey, id ], newProfile);
|
||||
await this.kv.getKv().set([ ProfileManagerBase.profilesKey, newUsername ], id);
|
||||
|
||||
await this.server.Platforms.addCachedLogin(platform, platformId, id);
|
||||
|
||||
return this.get(id);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { RoomInstance } from "../instances/base.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 SignalRSocketHandler } from "../socket/signalr/socket.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 ProfileManagerBase from "./manager.ts";
|
||||
import { recNetAccountSchema, SelfAccount, type RecNetAccount } from "./types/profile.ts";
|
||||
@@ -13,12 +16,16 @@ class Profile {
|
||||
#kv: KV;
|
||||
|
||||
#socket: SignalRSocketHandler | null = null;
|
||||
#server: ServerBase;
|
||||
#instance: RoomInstance | null = null;
|
||||
|
||||
#server: ServerBase;
|
||||
#selfAcc: SelfAccount;
|
||||
|
||||
managers: ProfileContentManager[] = [];
|
||||
|
||||
Settings: ProfileSettingsManager;
|
||||
Avatar: ProfileAvatarManager;
|
||||
Matchmaking: ProfileMatchmakingManager;
|
||||
|
||||
constructor(acc: SelfAccount, kv: KV, server: ServerBase) {
|
||||
this.#id = acc.accountId;
|
||||
@@ -28,6 +35,7 @@ class Profile {
|
||||
|
||||
this.Settings = new ProfileSettingsManager(this, this.#kv);
|
||||
this.Avatar = new ProfileAvatarManager(this, this.#kv);
|
||||
this.Matchmaking = new ProfileMatchmakingManager(this, this.#kv);
|
||||
}
|
||||
|
||||
async #saveSelfAcc() {
|
||||
@@ -67,11 +75,12 @@ class Profile {
|
||||
async setBio(bio: string) {
|
||||
const key = this.constructProfilePropertyKey('bio');
|
||||
await this.#kv.getKv().set(key, bio);
|
||||
this.#server.emit('profile.update', { profile: this });
|
||||
}
|
||||
|
||||
async getRole(): Promise<ProfileRole> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -80,6 +89,42 @@ class Profile {
|
||||
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() {
|
||||
return this.#id;
|
||||
}
|
||||
@@ -102,6 +147,10 @@ class Profile {
|
||||
return this.#selfAcc;
|
||||
}
|
||||
|
||||
same(profile: Profile) {
|
||||
return profile.getId() == this.getId();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Profile;
|
||||
@@ -1,12 +1,21 @@
|
||||
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({
|
||||
accountId: z.number(),
|
||||
profileImage: z.string(),
|
||||
isJunior: z.optional(z.boolean()),
|
||||
isJunior: z.coerce.boolean().optional(),
|
||||
username: 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) => {
|
||||
const d = new Date(arg);
|
||||
if (isNaN(d.getTime())) {
|
||||
@@ -24,14 +33,4 @@ export const selfAccountSchema = recNetAccountSchema.extend({
|
||||
});
|
||||
|
||||
export type RecNetAccount = z.infer<typeof recNetAccountSchema>;
|
||||
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>;
|
||||
export type SelfAccount = z.infer<typeof selfAccountSchema>;
|
||||
@@ -1,15 +1,19 @@
|
||||
import { AvatarContentBase } from "./avatars/base.ts";
|
||||
import { EventManager } from "./baseevent.ts";
|
||||
import { CommandsBase } from "./commands/commands.ts";
|
||||
import { ServerContentManager } from "./content/base.ts";
|
||||
import GameConfigsBase from "./gameconfigs/base.ts";
|
||||
import { InstanceManager } from "./instances/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 RoleUpdateEvent } from "./profiles/events/RoleUpdate.ts";
|
||||
import ProfileManagerBase from "./profiles/manager.ts";
|
||||
|
||||
interface ServerEvents {
|
||||
'profile.roleupdate': RoleUpdateEvent,
|
||||
'profile.update': ProfileUpdateEvent
|
||||
'profile.update': ProfileUpdateEvent,
|
||||
'presence.update': PresenceUpdateEvent,
|
||||
}
|
||||
|
||||
class ServerBase extends EventManager<ServerEvents> {
|
||||
@@ -18,6 +22,8 @@ class ServerBase extends EventManager<ServerEvents> {
|
||||
Commands = new CommandsBase(this, 'commands');
|
||||
Platforms = new PlatformsManager(this, 'platforms', true);
|
||||
Avatars = new AvatarContentBase(this, 'avatars');
|
||||
Instances = new InstanceManager(this, 'instances');
|
||||
Content = new ServerContentManager(this, "content");
|
||||
}
|
||||
|
||||
const Server = new ServerBase();
|
||||
|
||||
@@ -72,7 +72,7 @@ export class SignalRSocketHandler {
|
||||
this.sendRaw({});
|
||||
return;
|
||||
} 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
|
||||
const res = await this.#dispatchTarget(message.data.target, message.data.arguments[0]); // rec room only uses the first index
|
||||
if (res.type == TargetResultType.Success) {
|
||||
|
||||
@@ -5,7 +5,9 @@ export class PlayerSocketSubscriptionTarget extends SocketTarget {
|
||||
|
||||
#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[]) {
|
||||
this.#ids = ids;
|
||||
|
||||
@@ -5,7 +5,7 @@ export class SocketTarget {
|
||||
|
||||
socket: SignalRSocketHandler;
|
||||
|
||||
zod: z.ZodTuple = z.tuple([]);
|
||||
zod: z.ZodObject = z.object({});
|
||||
|
||||
constructor(socket: SignalRSocketHandler) {
|
||||
this.socket = socket;
|
||||
|
||||
@@ -150,14 +150,13 @@ export enum PushNotificationId {
|
||||
RelationshipChanged = 1,
|
||||
MessageReceived,
|
||||
MessageDeleted,
|
||||
PresenceHeartbeatResponse,
|
||||
PresenceHeartbeatResponse, // unused by the game
|
||||
RefreshLogin,
|
||||
Logout,
|
||||
SubscriptionUpdateProfile = 11,
|
||||
SubscriptionUpdatePresence,
|
||||
SubscriptionUpdateGameSession,
|
||||
SubscriptionUpdateRoom = 15,
|
||||
SubscriptionUpdateRoomPlaylist,
|
||||
ModerationQuitGame = 20,
|
||||
ModerationUpdateRequired,
|
||||
ModerationKick,
|
||||
@@ -166,7 +165,6 @@ export enum PushNotificationId {
|
||||
ServerMaintenance,
|
||||
GiftPackageReceived = 30,
|
||||
GiftPackageReceivedImmediate,
|
||||
GiftPackageRewardSelectionReceived,
|
||||
ProfileJuniorStatusUpdate = 40,
|
||||
RelationshipsInvalid = 50,
|
||||
StorefrontBalanceAdd = 60,
|
||||
@@ -184,7 +182,4 @@ export enum PushNotificationId {
|
||||
CommunityBoardUpdate = 95,
|
||||
CommunityBoardAnnouncementUpdate,
|
||||
InventionModerationStateChanged = 100,
|
||||
FreeGiftButtonItemsAdded = 110,
|
||||
LocalRoomKeyCreated = 120,
|
||||
LocalRoomKeyDeleted
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import Logging from "@proxnet/undead-logging";
|
||||
import z from "zod";
|
||||
import { verify } from "@hono/hono/jwt";
|
||||
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");
|
||||
|
||||
@@ -34,8 +34,8 @@ export async function authenticate(c: Context<HonoEnv>, nxt: Next) {
|
||||
if (authHeader.success) {
|
||||
|
||||
try {
|
||||
const payload = await verify(authHeader.data ? authHeader.data : 'not a valid token', secret);
|
||||
const profile = await Server.Profiles.get((payload as ProfileToken).sub);
|
||||
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 TokenFormat).sub);
|
||||
if (!profile) return c.json(genericResponse(false, "Internal Server Error"), 500);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,3 @@
|
||||
import { Context } from "@hono/hono";
|
||||
import { getConnInfo } from "@hono/hono/deno";
|
||||
|
||||
export function getSourceAddress(req: Request, netAddr?: Deno.NetAddr) {
|
||||
let addr = '(unknown src)';
|
||||
|
||||
@@ -14,20 +11,7 @@ export function getSourceAddress(req: Request, netAddr?: Deno.NetAddr) {
|
||||
if (first) addr = first;
|
||||
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) {
|
||||
const params = url.searchParams.toString();
|
||||
return `${url.pathname}${params ? `?${params}` : ''}`;
|
||||
|
||||
15
src/util/photon.ts
Normal file
15
src/util/photon.ts
Normal 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"
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import type { PersonaState, CommunityVisibilityState } from "./steam.ts";
|
||||
export interface SteamPlayer {
|
||||
steamid: string;
|
||||
personaname: string;
|
||||
realname?: string;
|
||||
profileurl: string;
|
||||
avatar: string;
|
||||
avatarmedium: string;
|
||||
|
||||
@@ -45,7 +45,7 @@ export enum CommunityVisibilityState {
|
||||
class SteamBase {
|
||||
|
||||
async GetPlayerSummaries(steamids: string[]) {
|
||||
if (!steamkey) return null;
|
||||
if (!steamkey) return [];
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('key', steamkey);
|
||||
@@ -53,13 +53,13 @@ class SteamBase {
|
||||
|
||||
try {
|
||||
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[] } };
|
||||
return resjson.response.players;
|
||||
} catch (err) {
|
||||
log.e(`Could not fetch Steam player summaries: ${(err as Error).stack}`);
|
||||
return null;
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user