forked from zombieb/galvanic-corrosion-rewrite
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_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"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
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 { 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
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 { 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 => {
|
||||||
|
|||||||
@@ -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({
|
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');
|
||||||
|
|||||||
@@ -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 || []);
|
||||||
});
|
});
|
||||||
@@ -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");
|
||||||
});
|
});
|
||||||
@@ -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.use(authenticate);
|
||||||
|
|
||||||
route.app.post('/login', async c => {
|
route.app.post('/login', _c => {
|
||||||
return c.status(200);
|
return new Response("OK", { status: 200 });
|
||||||
});
|
});
|
||||||
@@ -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();
|
||||||
});
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { 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'],
|
||||||
|
|||||||
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 { 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
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";
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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>;
|
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
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 {
|
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;
|
||||||
|
|||||||
@@ -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 [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user