diff --git a/fortnite/external.go b/fortnite/external.go index 10d145d..757a5ab 100644 --- a/fortnite/external.go +++ b/fortnite/external.go @@ -89,7 +89,6 @@ func (c *ExternalDataClient) LoadExternalData() { for _, setValue := range addNumericStylesToSets { set, found := c.FortniteSets[setValue] if !found { - aid.Print("set not found: " + setValue) continue } diff --git a/fortnite/person.go b/fortnite/person.go index 3c7a8a5..032c0e0 100644 --- a/fortnite/person.go +++ b/fortnite/person.go @@ -152,6 +152,10 @@ func NewFortnitePersonWithId(id string, displayName string, everything bool) *p. person.CommonCoreProfile.Attributes.AddAttribute(p.NewAttribute("allowed_to_send_gifts", true)).Save() person.CommonCoreProfile.Attributes.AddAttribute(p.NewAttribute("gift_history", aid.JSON{})).Save() + person.CommonCoreProfile.Attributes.AddAttribute(p.NewAttribute("party.recieveIntents", "ALL")).Save() + person.CommonCoreProfile.Attributes.AddAttribute(p.NewAttribute("party.recieveInvites", "ALL")).Save() + + loadout := p.NewLoadout("PRESET 1", person.AthenaProfile) person.AthenaProfile.Loadouts.AddLoadout(loadout).Save() person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("loadouts", []string{loadout.ID})).Save() diff --git a/fortnite/shop.go b/fortnite/shop.go index 9af48f8..35917f8 100644 --- a/fortnite/shop.go +++ b/fortnite/shop.go @@ -415,7 +415,7 @@ func GenerateRandomStorefront() { } minimumSets := 4 - if aid.Config.Fortnite.Season < 11 { + if aid.Config.Fortnite.Season <+ 12 { minimumSets = 3 } diff --git a/handlers/auth.go b/handlers/auth.go index f9edba1..147a3bf 100644 --- a/handlers/auth.go +++ b/handlers/auth.go @@ -44,12 +44,31 @@ func PostTokenClientCredentials(c *fiber.Ctx, body *FortniteTokenBody) error { return c.Status(400).JSON(aid.ErrorBadRequest("Client Credentials is disabled.")) } + clientCredentials, err := aid.JWTSign(aid.JSON{ + "creation_date": time.Now().Format("2006-01-02T15:04:05.999Z"), + "clsvc": "prod-fn", + "t": "s", + "mver": false, + "clid": aid.Hash([]byte(c.IP())), + "ic": true, + "exp": 1707772234, + "iat": 1707757834, + "jti": "snow-revoke", + "pfpid": "prod-fn", + "am": "client_credentials", + }) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(aid.ErrorInternalServer) + } + return c.Status(200).JSON(aid.JSON{ - "access_token": "snow", + "access_token": clientCredentials, + "application_id": "fghi4567FNFBKFz3E4TROb0bmPS8h1GW", "token_type": "bearer", "client_id": aid.Hash([]byte(c.IP())), - "client_service": "fortnite", + "client_service": "prod-fn", "internal_client": true, + "product_id": "prod-fn", "expires_in": 3600, "expires_at": time.Now().Add(time.Hour).Format("2006-01-02T15:04:05.999Z"), }) diff --git a/handlers/parties.go b/handlers/parties.go index 2fce347..a712af4 100644 --- a/handlers/parties.go +++ b/handlers/parties.go @@ -1,8 +1,11 @@ package handlers import ( + "time" + "github.com/ectrc/snow/aid" p "github.com/ectrc/snow/person" + "github.com/ectrc/snow/socket" "github.com/gofiber/fiber/v2" ) @@ -22,4 +25,168 @@ func GetUserParties(c *fiber.Ctx) error { }) return c.Status(200).JSON(response) +} + +func GetUserPartyPrivacy(c *fiber.Ctx) error { + person := c.Locals("person").(*p.Person) + + recieveIntents := person.CommonCoreProfile.Attributes.GetAttributeByKey("party.recieveIntents") + if recieveIntents == nil { + return c.Status(400).JSON(aid.ErrorBadRequest("No Privacy Found")) + } + + recieveInvites := person.CommonCoreProfile.Attributes.GetAttributeByKey("party.recieveInvites") + if recieveIntents == nil { + return c.Status(400).JSON(aid.ErrorBadRequest("No Privacy Found")) + } + + return c.Status(200).JSON(aid.JSON{ + "recieveIntents": aid.JSONParse(recieveIntents.ValueJSON), + "recieveInvites": aid.JSONParse(recieveInvites.ValueJSON), + }) +} + +func GetUserPartyNotifications(c *fiber.Ctx) error { + return c.Status(200).JSON(aid.JSON{ + "pings": 0, + "invites": 0, + }) +} + +func GetPartyForMember(c *fiber.Ctx) error { + person := c.Locals("person").(*p.Person) + + party, ok := person.Parties.Get(c.Params("partyId")) + if !ok { + return c.Status(400).JSON(aid.ErrorBadRequest("Party Not Found")) + } + + return c.Status(200).JSON(party.GenerateFortniteParty()) +} + +func PostCreateParty(c *fiber.Ctx) error { + person := c.Locals("person").(*p.Person) + + person.Parties.Range(func(key string, party *p.Party) bool { + party.RemoveMember(person) + return true + }) + + var body struct { + Config map[string]interface{} `json:"config"` + Meta map[string]interface{} `json:"meta"` + JoinInformation struct { + Meta map[string]interface{} `json:"meta"` + Connection aid.JSON `json:"connection"` + } `json:"join_info"` + } + + if err := c.BodyParser(&body); err != nil { + return c.Status(400).JSON(aid.ErrorBadRequest("Invalid Request")) + } + + party := p.NewParty() + party.UpdateMeta(body.Meta) + party.UpdateConfig(body.Config) + + party.AddMember(person, "CAPTAIN") + party.UpdateMemberMeta(person, body.JoinInformation.Meta) + + body.JoinInformation.Connection["connected_at"] = time.Now().Format(time.RFC3339) + body.JoinInformation.Connection["updated_at"] = time.Now().Format(time.RFC3339) + party.UpdateMemberConnections(person, body.JoinInformation.Connection) + + member := party.GetMember(person) + if member == nil { + return c.Status(400).JSON(aid.ErrorBadRequest("Failed to create party")) + } + + s, ok := socket.JabberSockets.Get(person.ID) + if !ok { + return c.Status(400).JSON(aid.ErrorBadRequest("No socket connection found")) + } + + s.JabberSendMessageToPerson(aid.JSON{ + "ns": "Fortnite", + "party_id": party.ID, + "account_id": person.ID, + "account_dn": person.DisplayName, + "connection": body.JoinInformation.Connection, + "member_state_updated": member.Meta, + "updated_at": member.UpdatedAt.Format(time.RFC3339), + "joined_at": member.JoinedAt.Format(time.RFC3339), + "sent": time.Now().Format(time.RFC3339), + "revision": 0, + "type": "com.epicgames.social.party.notification.v0.MEMBER_JOINED", + }) + + return c.Status(200).JSON(party.GenerateFortniteParty()) +} + +func PatchUpdateParty(c *fiber.Ctx) error { + person := c.Locals("person").(*p.Person) + + var body struct { + Config map[string]interface{} `json:"config"` + Meta struct { + Update map[string]interface{} `json:"update"` + Delete []string `json:"delete"` + } `json:"meta"` + } + + if err := c.BodyParser(&body); err != nil { + return c.Status(400).JSON(aid.ErrorBadRequest("Invalid Request")) + } + + party, ok := person.Parties.Get(c.Params("partyId")) + if !ok { + return c.Status(400).JSON(aid.ErrorBadRequest("Party Not Found")) + } + + member := party.GetMember(person) + if member == nil { + return c.Status(400).JSON(aid.ErrorBadRequest("Not in Party")) + } + + if member.Role != "CAPTAIN" { + return c.Status(400).JSON(aid.ErrorBadRequest("Not Captain")) + } + + party.UpdateConfig(body.Config) + party.UpdateMeta(body.Meta.Update) + party.DeleteMeta(body.Meta.Delete) + + return c.Status(200).JSON(party.GenerateFortniteParty()) +} + +func PatchUpdatePartyMemberMeta(c *fiber.Ctx) error { + person := c.Locals("person").(*p.Person) + + var body struct { + Update map[string]interface{} `json:"update"` + Delete []string `json:"delete"` + } + + if err := c.BodyParser(&body); err != nil { + return c.Status(400).JSON(aid.ErrorBadRequest("Invalid Request")) + } + + party, ok := person.Parties.Get(c.Params("partyId")) + if !ok { + return c.Status(400).JSON(aid.ErrorBadRequest("Party Not Found")) + } + + member := party.GetMember(person) + if member == nil { + return c.Status(400).JSON(aid.ErrorBadRequest("Not in Party")) + } + + if c.Params("accountId") != person.ID { + return c.Status(400).JSON(aid.ErrorBadRequest("Not owner of person")) + } + + party.UpdateMemberMeta(person, body.Update) + party.DeleteMemberMeta(person, body.Delete) + + return c.Status(200).JSON(party.GenerateFortniteParty()) } \ No newline at end of file diff --git a/handlers/snow.go b/handlers/snow.go index c9cdcff..8dcf62b 100644 --- a/handlers/snow.go +++ b/handlers/snow.go @@ -16,7 +16,7 @@ func MiddlewareOnlyDebug(c *fiber.Ctx) error { return c.SendStatus(403) } -func GetPreloadedCosmetics(c *fiber.Ctx) error { +func GetSnowPreloadedCosmetics(c *fiber.Ctx) error { return c.JSON(fortnite.External) } @@ -42,7 +42,7 @@ func GetPlayer(c *fiber.Ctx) error { }) } -func GetCachedPlayers(c *fiber.Ctx) error { +func GetSnowCachedPlayers(c *fiber.Ctx) error { persons := p.AllFromCache() players := make([]p.PersonSnapshot, len(persons)) @@ -59,3 +59,18 @@ func GetSnowConfig(c *fiber.Ctx) error { "amazon": storage.Repo.Amazon, }) } + +func GetSnowParties(c *fiber.Ctx) error { + parties := []aid.JSON{} + + p.Parties.Range(func(key string, value *p.Party) bool { + parties = append(parties, value.GenerateFortniteParty()) + return true + }) + + return c.JSON(parties) +} + +func GetSnowShop(c *fiber.Ctx) error { + return c.JSON(fortnite.StaticCatalog) +} \ No newline at end of file diff --git a/handlers/socket.go b/handlers/socket.go index b6db557..be0f534 100644 --- a/handlers/socket.go +++ b/handlers/socket.go @@ -43,7 +43,7 @@ func WebsocketConnection(c *websocket.Conn) { } } -func GetConnectedSockets(c *fiber.Ctx) error { +func GetSnowConnectedSockets(c *fiber.Ctx) error { jabber := aid.JSON{} socket.JabberSockets.Range(func(key string, value *socket.Socket[socket.JabberData]) bool { jabber[key] = value diff --git a/main.go b/main.go index b738d89..d0cfa14 100644 --- a/main.go +++ b/main.go @@ -120,12 +120,9 @@ func main() { storage.Get("/system", handlers.GetCloudStorageFiles) storage.Get("/system/config", handlers.GetCloudStorageConfig) storage.Get("/system/:fileName", handlers.GetCloudStorageFile) - - user := storage.Group("/user") - user.Use(handlers.MiddlewareFortnite) - user.Get("/:accountId", handlers.GetUserStorageFiles) - user.Get("/:accountId/:fileName", handlers.GetUserStorageFile) - user.Put("/:accountId/:fileName", handlers.PutUserStorageFile) + storage.Get("/user/:accountId", handlers.MiddlewareFortnite, handlers.GetUserStorageFiles) + storage.Get("/user/:accountId/:fileName", handlers.MiddlewareFortnite, handlers.GetUserStorageFile) + storage.Put("/user/:accountId/:fileName", handlers.MiddlewareFortnite, handlers.PutUserStorageFile) friends := r.Group("/friends/api") friends.Use(handlers.MiddlewareFortnite) @@ -155,13 +152,28 @@ func main() { party := r.Group("/party/api/v1/Fortnite") party.Use(handlers.MiddlewareFortnite) party.Get("/user/:accountId", handlers.GetUserParties) + party.Get("/user/:accountId/settings/privacy", handlers.GetUserPartyPrivacy) + party.Get("/user/:accountId/notifications/undelivered/count", handlers.GetUserPartyNotifications) + party.Post("/parties", handlers.PostCreateParty) + party.Get("/parties/:partyId", handlers.GetPartyForMember) + party.Patch("/parties/:partyId", handlers.PatchUpdateParty) + party.Patch("/parties/:partyId/members/:accountId/meta", handlers.PatchUpdatePartyMemberMeta) + // post /parties/:partyId/members/:accountId/conferences/connection (join a voip channel) + // delete /parties/:partyId/members/:accountid (remove a person from a party) + // get /user/:accountId/pings/:pinger/friendId/parties (get pings from a friend) + // post /user/:accountId/pings/:pinger/join (join a party from a ping) + // post /user/:friendId/pings/:accountId (send a ping) + // delete /user/:accountId/pings/:pinger/friendId (delete pings) + // post /members/:friendId/intentions/:accountId (send an invite and add invite to party) snow := r.Group("/snow") snow.Use(handlers.MiddlewareOnlyDebug) - snow.Get("/cache", handlers.GetCachedPlayers) + snow.Get("/cache", handlers.GetSnowCachedPlayers) snow.Get("/config", handlers.GetSnowConfig) - snow.Get("/sockets", handlers.GetConnectedSockets) - snow.Get("/cosmetics", handlers.GetPreloadedCosmetics) + snow.Get("/sockets", handlers.GetSnowConnectedSockets) + snow.Get("/cosmetics", handlers.GetSnowPreloadedCosmetics) + snow.Get("/parties", handlers.GetSnowParties) + snow.Get("/shop", handlers.GetSnowShop) discord := snow.Group("/discord") discord.Get("/", handlers.GetDiscordOAuthURL) diff --git a/person/parties.go b/person/parties.go index b36ca4f..6edef09 100644 --- a/person/parties.go +++ b/person/parties.go @@ -8,20 +8,44 @@ import ( "github.com/google/uuid" ) +type PartyPing struct{ + Person *Person + Party *Party +} + type PartyMember struct{ Person *Person ConnectionID string Meta map[string]interface{} Connections map[string]aid.JSON Role string + JoinedAt time.Time + UpdatedAt time.Time +} + +func (pm *PartyMember) GenerateFortnitePartyMember() aid.JSON { + connections := []aid.JSON{} + for _, connection := range pm.Connections { + connections = append(connections, connection) + } + + return aid.JSON{ + "account_id": pm.Person.ID, + "role": pm.Role, + "meta": pm.Meta, + "joined_at": pm.JoinedAt.Format(time.RFC3339), + "connections": connections, + "revision": 0, + } } type Party struct{ ID string - Members []*PartyMember + Members map[string]*PartyMember Config map[string]interface{} Meta map[string]interface{} m sync.Mutex + CreatedAt time.Time } var ( @@ -31,16 +55,29 @@ var ( func NewParty() *Party { party := &Party{ ID: uuid.New().String(), - Members: []*PartyMember{}, - Config: make(map[string]interface{}), + Members: make(map[string]*PartyMember), + Config: map[string]interface{}{ + "type": "DEFAULT", + "sub_type": "default", + "intention_ttl:": 60, + "invite_ttl:": 60, + }, Meta: make(map[string]interface{}), + CreatedAt: time.Now(), } Parties.Set(party.ID, party) return party } -func (p *Party) AddMember(person *Person) { +func (p *Party) GetMember(person *Person) *PartyMember { + p.m.Lock() + defer p.m.Unlock() + + return p.Members[person.ID] +} + +func (p *Party) AddMember(person *Person, role string) { p.m.Lock() defer p.m.Unlock() @@ -48,10 +85,11 @@ func (p *Party) AddMember(person *Person) { Person: person, Meta: make(map[string]interface{}), Connections: make(map[string]aid.JSON), - Role: "MEMBER", + Role: role, + JoinedAt: time.Now(), } - p.Members = append(p.Members, partyMember) + p.Members[person.ID] = partyMember person.Parties.Set(p.ID, p) // xmpp to person and rest of party to say new member! } @@ -60,77 +98,91 @@ func (p *Party) RemoveMember(person *Person) { p.m.Lock() defer p.m.Unlock() - for i, member := range p.Members { - if member.Person == person { - p.Members = append(p.Members[:i], p.Members[i+1:]...) - break - } - } - + delete(p.Members, person.ID) if len(p.Members) == 0 { Parties.Delete(p.ID) } person.Parties.Delete(p.ID) - // xmpp to person and rest of party to say member left! } -func (p *Party) UpdateMeta(key string, value interface{}) { +func (p *Party) UpdateMeta(m map[string]interface{}) { p.m.Lock() defer p.m.Unlock() - p.Meta[key] = value - // xmpp to rest of party to say meta updated! -} - -func (p *Party) DeleteMeta(key string) { - p.m.Lock() - defer p.m.Unlock() - - delete(p.Meta, key) - // xmpp to rest of party to say meta deleted! -} - -func (p *Party) UpdateMemberMeta(person *Person, key string, value interface{}) { - p.m.Lock() - defer p.m.Unlock() - - for _, member := range p.Members { - if member.Person == person { - member.Meta[key] = value - // xmpp to person and rest of party to say member meta updated! - break - } + for key, value := range m { + p.Meta[key] = value } } -func (p *Party) DeleteMemberMeta(person *Person, key string) { +func (p *Party) DeleteMeta(keys []string) { p.m.Lock() defer p.m.Unlock() - for _, member := range p.Members { - if member.Person == person { - delete(member.Meta, key) - // xmpp to person and rest of party to say member meta deleted! - break - } + for _, key := range keys { + delete(p.Meta, key) } } -func (p *Party) UpdateConfig(key string, value interface{}) { +func (p *Party) UpdateMemberMeta(person *Person, m map[string]interface{}) { p.m.Lock() defer p.m.Unlock() - p.Config[key] = value - // xmpp to rest of party to say config updated! + member, ok := p.Members[person.ID] + if !ok { + return + } + + for key, value := range m { + member.Meta[key] = value + } } -func (p *Party) DeleteConfig(key string) { +func (p *Party) UpdateMemberRevision(person *Person, revision int) { p.m.Lock() defer p.m.Unlock() - delete(p.Config, key) - // xmpp to rest of party to say config deleted! + member, ok := p.Members[person.ID] + if !ok { + return + } + + member.Meta["revision"] = revision +} + +func (p *Party) DeleteMemberMeta(person *Person, keys []string) { + p.m.Lock() + defer p.m.Unlock() + + member, ok := p.Members[person.ID] + if !ok { + return + } + + for _, key := range keys { + delete(member.Meta, key) + } +} + +func (p *Party) UpdateMemberConnections(person *Person, m aid.JSON) { + p.m.Lock() + defer p.m.Unlock() + + member, ok := p.Members[person.ID] + if !ok { + return + } + + member.Connections[m["id"].(string)] = m +} + +func (p *Party) UpdateConfig(m map[string]interface{}) { + p.m.Lock() + defer p.m.Unlock() + + for key, value := range m { + p.Config[key] = value + } } func (p *Party) GenerateFortniteParty() aid.JSON { @@ -139,21 +191,19 @@ func (p *Party) GenerateFortniteParty() aid.JSON { party := aid.JSON{ "id": p.ID, - "members": aid.JSON{}, "config": p.Config, "meta": p.Meta, - "created_at": "0000-00-00T00:00:00Z", + "applicants": []aid.JSON{}, + "members": []aid.JSON{}, + "invites": []aid.JSON{}, + "intentions": []aid.JSON{}, + "created_at": p.CreatedAt.Format(time.RFC3339), "updated_at": time.Now().Format(time.RFC3339), "revision": 0, } for _, member := range p.Members { - party["members"].(aid.JSON)[member.Person.ID] = aid.JSON{ - "account_id": member.Person.ID, - "role": member.Role, - "meta": member.Meta, - "connections": member.Connections, - } + party["members"] = append(party["members"].([]aid.JSON), member.GenerateFortnitePartyMember()) } return party diff --git a/person/person.go b/person/person.go index 09571ea..d67ad52 100644 --- a/person/person.go +++ b/person/person.go @@ -23,6 +23,7 @@ type Person struct { BanHistory aid.GenericSyncMap[storage.DB_BanStatus] Relationships aid.GenericSyncMap[Relationship] Parties aid.GenericSyncMap[Party] + Pings aid.GenericSyncMap[PartyPing] } func NewPerson() *Person { @@ -37,6 +38,10 @@ func NewPerson() *Person { Profile0Profile: NewProfile("profile0"), CollectionsProfile: NewProfile("collections"), CreativeProfile: NewProfile("creative"), + BanHistory: aid.GenericSyncMap[storage.DB_BanStatus]{}, + Relationships: aid.GenericSyncMap[Relationship]{}, + Parties: aid.GenericSyncMap[Party]{}, + Pings: aid.GenericSyncMap[PartyPing]{}, } } @@ -52,6 +57,10 @@ func NewPersonWithCustomID(id string) *Person { Profile0Profile: NewProfile("profile0"), CollectionsProfile: NewProfile("collections"), CreativeProfile: NewProfile("creative"), + BanHistory: aid.GenericSyncMap[storage.DB_BanStatus]{}, + Relationships: aid.GenericSyncMap[Relationship]{}, + Parties: aid.GenericSyncMap[Party]{}, + Pings: aid.GenericSyncMap[PartyPing]{}, } } @@ -199,6 +208,7 @@ func findHelper(databasePerson *storage.DB_Person, shallow bool, save bool) *Per Discord: &databasePerson.Discord, RefundTickets: databasePerson.RefundTickets, Relationships: aid.GenericSyncMap[Relationship]{}, + Parties: aid.GenericSyncMap[Party]{}, } for _, ban := range databasePerson.BanHistory { diff --git a/storage/hotfix.go b/storage/hotfix.go index 8609824..21dc4ba 100644 --- a/storage/hotfix.go +++ b/storage/hotfix.go @@ -5,6 +5,11 @@ import ( ) func GetDefaultEngine() []byte { + /*[OnlineSubsystemMcp] +bUsePartySystemV2=true + +[OnlineSubsystemMcp.OnlinePartySystemMcpAdapter] +bUsePartySystemV2=true*/ return []byte(` [OnlineSubsystemMcp.Xmpp] bUseSSL=false @@ -16,12 +21,6 @@ bUseSSL=false Protocol=ws ServerAddr="ws://`+ aid.Config.API.Host + aid.Config.API.Port +`/?" -[OnlineSubsystemMcp] -bUsePartySystemV2=true - -[OnlineSubsystemMcp.OnlinePartySystemMcpAdapter] -bUsePartySystemV2=true - [XMPP] bEnableWebsockets=true