diff --git a/aid/syncer.go b/aid/syncer.go index 94ec55a..f5cae4f 100644 --- a/aid/syncer.go +++ b/aid/syncer.go @@ -21,6 +21,15 @@ func (s *GenericSyncMap[T]) Get(key string) (*T, bool) { return v.(*T), true } +func (s *GenericSyncMap[T]) MustGet(key string) *T { + v, ok := s.m.Load(key) + if !ok { + return nil + } + + return v.(*T) +} + func (s *GenericSyncMap[T]) Delete(key string) { s.m.Delete(key) } @@ -29,4 +38,9 @@ func (s *GenericSyncMap[T]) Range(f func(key string, value *T) bool) { s.m.Range(func(key, value interface{}) bool { return f(key.(string), value.(*T)) }) +} + +func (s *GenericSyncMap[T]) Has(key string) bool { + _, ok := s.Get(key) + return ok } \ No newline at end of file diff --git a/handlers/friends.go b/handlers/friends.go index 7ec9b8a..afacbde 100644 --- a/handlers/friends.go +++ b/handlers/friends.go @@ -1,15 +1,83 @@ package handlers import ( + "fmt" + "time" + "github.com/ectrc/snow/aid" + p "github.com/ectrc/snow/person" + "github.com/ectrc/snow/socket" "github.com/gofiber/fiber/v2" ) func GetFriendList(c *fiber.Ctx) error { - return c.Status(200).JSON([]aid.JSON{}) + person := c.Locals("person").(*p.Person) + result := []aid.JSON{} + + person.IncomingRelationships.Range(func(key string, value *p.Relationship[p.RelationshipInboundDirection]) bool { + result = append(result, value.GenerateFortniteFriendEntry()) + return true + }) + + person.OutgoingRelationships.Range(func(key string, value *p.Relationship[p.RelationshipOutboundDirection]) bool { + result = append(result, value.GenerateFortniteFriendEntry()) + return true + }) + + return c.Status(200).JSON(result) } func PostCreateFriend(c *fiber.Ctx) error { + person := c.Locals("person").(*p.Person) + wanted := c.Params("wanted") + + direction, err := person.CreateRelationship(wanted) + if err != nil { + aid.Print(fmt.Sprintf("Error creating relationship: %s", err.Error())) + return c.Status(400).JSON(aid.ErrorBadRequest(err.Error())) + } + + socket.JabberSockets.Range(func(key string, value *socket.Socket[socket.JabberData]) bool { + aid.Print(fmt.Sprintf("Checking socket: %s", key, value)) + return true + }) + + personSocket, found := socket.GetJabberSocketByPersonID(wanted) + aid.Print(fmt.Sprintf("Found socket: %t", found), fmt.Sprintf("Direction: %s", direction)) + if found { + payload := aid.JSON{} + switch direction { + case "INBOUND": + payload = personSocket.Person.IncomingRelationships.MustGet(wanted).GenerateFortniteFriendEntry() + case "OUTBOUND": + payload = personSocket.Person.OutgoingRelationships.MustGet(wanted).GenerateFortniteFriendEntry() + } + + personSocket.JabberSendMessageToPerson(aid.JSON{ + "type": "com.epicgames.friends.core.apiobjects.Friend", + "timestamp": time.Now().Format(time.RFC3339), + "payload": payload, + }) + } + + friendSocket, found := socket.GetJabberSocketByPersonID(wanted) + aid.Print(fmt.Sprintf("Found friend socket: %t", found), fmt.Sprintf("Direction: %s", direction)) + if found { + payload := aid.JSON{} + switch direction { + case "INBOUND": + payload = friendSocket.Person.OutgoingRelationships.MustGet(person.ID).GenerateFortniteFriendEntry() + case "OUTBOUND": + payload = friendSocket.Person.IncomingRelationships.MustGet(person.ID).GenerateFortniteFriendEntry() + } + + friendSocket.JabberSendMessageToPerson(aid.JSON{ + "type": "com.epicgames.friends.core.apiobjects.Friend", + "timestamp": time.Now().Format(time.RFC3339), + "payload": payload, + }) + } + return c.SendStatus(204) } diff --git a/handlers/socket.go b/handlers/socket.go index c81ec47..8efda12 100644 --- a/handlers/socket.go +++ b/handlers/socket.go @@ -13,19 +13,16 @@ func MiddlewareWebsocket(c *fiber.Ctx) error { return fiber.ErrUpgradeRequired } - var identifier string var protocol string switch c.Get("Sec-WebSocket-Protocol") { case "xmpp": - identifier = c.Get("Sec-WebSocket-Key") protocol = "jabber" default: protocol = "matchmaking" - identifier = uuid.New().String() } - c.Locals("identifier", identifier) + c.Locals("identifier", uuid.New().String()) c.Locals("protocol", protocol) return c.Next() @@ -44,4 +41,23 @@ func WebsocketConnection(c *websocket.Conn) { default: aid.Print("Invalid protocol: " + protocol) } +} + +func GetConnectedSockets(c *fiber.Ctx) error { + jabber := []socket.Socket[socket.JabberData]{} + socket.JabberSockets.Range(func(key string, value *socket.Socket[socket.JabberData]) bool { + jabber = append(jabber, *value) + return true + }) + + matchmaking := []socket.Socket[socket.MatchmakerData]{} + socket.MatchmakerSockets.Range(func(key string, value *socket.Socket[socket.MatchmakerData]) bool { + matchmaking = append(matchmaking, *value) + return true + }) + + return c.Status(200).JSON(aid.JSON{ + "jabber": jabber, + "matchmaking": matchmaking, + }) } \ No newline at end of file diff --git a/main.go b/main.go index 23c3a05..05bb62c 100644 --- a/main.go +++ b/main.go @@ -129,6 +129,7 @@ func main() { lightswitch.Get("/service/bulk/status", handlers.GetLightswitchBulkStatus) snow := r.Group("/snow") + snow.Get("/sockets", handlers.GetConnectedSockets) snow.Get("/cosmetics", handlers.GetPreloadedCosmetics) snow.Get("/image/:playlist", handlers.GetPlaylistImage) diff --git a/person/person.go b/person/person.go index da4ec6e..e41b06b 100644 --- a/person/person.go +++ b/person/person.go @@ -1,6 +1,7 @@ package person import ( + "github.com/ectrc/snow/aid" "github.com/ectrc/snow/storage" "github.com/google/uuid" ) @@ -17,6 +18,8 @@ type Person struct { CollectionsProfile *Profile CreativeProfile *Profile Discord *storage.DB_DiscordPerson + OutgoingRelationships aid.GenericSyncMap[Relationship[RelationshipOutboundDirection]] + IncomingRelationships aid.GenericSyncMap[Relationship[RelationshipInboundDirection]] } func NewPerson() *Person { @@ -64,7 +67,25 @@ func Find(personId string) *Person { return nil } - return findHelper(person) + return findHelper(person, false, true) +} + +func FindShallow(personId string) *Person { + if cache == nil { + cache = NewPersonsCacheMutex() + } + + cachedPerson := cache.GetPerson(personId) + if cachedPerson != nil { + return cachedPerson + } + + person := storage.Repo.GetPersonFromDB(personId) + if person == nil { + return nil + } + + return findHelper(person, true, true) } func FindByDisplay(displayName string) *Person { @@ -82,7 +103,25 @@ func FindByDisplay(displayName string) *Person { return nil } - return findHelper(person) + return findHelper(person, false, true) +} + +func FindByDisplayShallow(displayName string) *Person { + if cache == nil { + cache = NewPersonsCacheMutex() + } + + cachedPerson := cache.GetPersonByDisplay(displayName) + if cachedPerson != nil { + return cachedPerson + } + + person := storage.Repo.GetPersonByDisplayFromDB(displayName) + if person == nil { + return nil + } + + return findHelper(person, true, true) } func FindByDiscord(discordId string) *Person { @@ -100,10 +139,10 @@ func FindByDiscord(discordId string) *Person { return nil } - return findHelper(person) + return findHelper(person, false, true) } -func findHelper(databasePerson *storage.DB_Person) *Person { +func findHelper(databasePerson *storage.DB_Person, shallow bool, save bool) *Person { athenaProfile := NewProfile("athena") commonCoreProfile := NewProfile("common_core") commonPublicProfile := NewProfile("common_public") @@ -155,9 +194,17 @@ func findHelper(databasePerson *storage.DB_Person) *Person { CollectionsProfile: collectionsProfile, CreativeProfile: creativeProfile, Discord: &databasePerson.Discord, + OutgoingRelationships: aid.GenericSyncMap[Relationship[RelationshipOutboundDirection]]{}, + IncomingRelationships: aid.GenericSyncMap[Relationship[RelationshipInboundDirection]]{}, } - cache.SavePerson(person) + if !shallow { + person.LoadRelationships() + } + + if save { + cache.SavePerson(person) + } return person } diff --git a/person/relationships.go b/person/relationships.go new file mode 100644 index 0000000..fccaba3 --- /dev/null +++ b/person/relationships.go @@ -0,0 +1,141 @@ +package person + +import ( + "fmt" + + "github.com/ectrc/snow/aid" + "github.com/ectrc/snow/storage" +) + +type RelationshipDirection string + +type RelationshipInboundDirection RelationshipDirection +const RelationshipInboundDirectionValue RelationshipInboundDirection = "INBOUND" + +type RelationshipOutboundDirection RelationshipDirection +const RelationshipOutboundDirectionValue RelationshipOutboundDirection = "OUTBOUND" + +type Relationship[T RelationshipInboundDirection | RelationshipOutboundDirection] struct { + Me *Person + Towards *Person + Status string + Direction T +} + +func (r *Relationship[T]) ToDatabase() *storage.DB_Relationship { + return &storage.DB_Relationship{ + IncomingPersonID: r.Me.ID, + OutgoingPersonID: r.Towards.ID, + Status: r.Status, + } +} + +func (r *Relationship[T]) GenerateFortniteFriendEntry() aid.JSON { + return aid.JSON{ + "accountId": r.Towards.ID, + "status": r.Status, + "direction": string(r.Direction), + "created": "0000-00-00T00:00:00.000Z", + "favorite": false, + } +} + +func (r *Relationship[T]) Save() { + storage.Repo.Storage.SaveRelationship(r.ToDatabase()) +} + +func (r *Relationship[T]) Delete() { + storage.Repo.Storage.DeleteRelationship(r.ToDatabase()) +} + +func (p *Person) LoadRelationships() { + incoming := storage.Repo.Storage.GetIncomingRelationships(p.ID) + for _, entry := range incoming { + relationship := &Relationship[RelationshipInboundDirection]{ + Status: entry.Status, + Me: p, + Towards: FindShallow(entry.OutgoingPersonID), + Direction: RelationshipInboundDirectionValue, + } + + p.IncomingRelationships.Set(entry.OutgoingPersonID, relationship) + } + + outgoing := storage.Repo.Storage.GetOutgoingRelationships(p.ID) + for _, entry := range outgoing { + relationship := &Relationship[RelationshipOutboundDirection]{ + Status: entry.Status, + Me: p, + Towards: FindShallow(entry.IncomingPersonID), + Direction: RelationshipOutboundDirectionValue, + } + + p.OutgoingRelationships.Set(entry.IncomingPersonID, relationship) + } +} + +func (p *Person) CreateRelationship(personId string) (string, error) { + if p.ID == personId { + return "", fmt.Errorf("cannot create relationship with yourself") + } + + if p.IncomingRelationships.Has(personId) { + return "INBOUND", p.createAcceptInboundRelationship(personId) + } + + return "OUTBOUND", p.createOutboundRelationship(personId) +} + +func (p *Person) createOutboundRelationship(towards string) error { + towardsPerson := Find(towards) + if towardsPerson == nil { + return fmt.Errorf("person not found") + } + + relationship := &Relationship[RelationshipOutboundDirection]{ + Me: p, + Towards: towardsPerson, + Status: "PENDING", + Direction: RelationshipOutboundDirectionValue, + } + relationship.Save() + p.OutgoingRelationships.Set(towards, relationship) + + tempRelationship := &Relationship[RelationshipInboundDirection]{ + Me: towardsPerson, + Towards: p, + Status: "PENDING", + Direction: RelationshipInboundDirectionValue, + } + tempRelationship.Save() + towardsPerson.IncomingRelationships.Set(p.ID, tempRelationship) + + return nil +} + +func (p *Person) createAcceptInboundRelationship(towards string) error { + towardsPerson := Find(towards) + if towardsPerson == nil { + return fmt.Errorf("person not found") + } + + relationship := &Relationship[RelationshipInboundDirection]{ + Me: p, + Towards: towardsPerson, + Status: "ACCEPTED", + Direction: RelationshipInboundDirectionValue, + } + relationship.Save() + p.IncomingRelationships.Set(towards, relationship) + + tempRelationship := &Relationship[RelationshipOutboundDirection]{ + Me: towardsPerson, + Towards: p, + Status: "ACCEPTED", + Direction: RelationshipOutboundDirectionValue, + } + tempRelationship.Save() + towardsPerson.OutgoingRelationships.Set(p.ID, tempRelationship) + + return nil +} \ No newline at end of file diff --git a/socket/jabber.go b/socket/jabber.go index 9fb01e8..c8f1759 100644 --- a/socket/jabber.go +++ b/socket/jabber.go @@ -2,6 +2,7 @@ package socket import ( "fmt" + "reflect" "github.com/beevik/etree" "github.com/ectrc/snow/aid" @@ -91,4 +92,39 @@ func jabberIqSetHandler(socket *Socket[JabberData], parsed *etree.Document) erro func jabberIqGetHandler(socket *Socket[JabberData], parsed *etree.Document) error { socket.Connection.WriteMessage(websocket.TextMessage, []byte(``)) return nil +} + +func GetJabberSocketByPersonID(id string) (*Socket[JabberData], bool) { + var found *Socket[JabberData] + + JabberSockets.Range(func(key string, socket *Socket[JabberData]) bool { + if socket.Person.ID == id { + found = socket + return false + } + + return true + }) + + return found, found != nil +} + +func (s *Socket[T]) JabberSendMessageToPerson(data aid.JSON) { + if reflect.TypeOf(s.Data) != reflect.TypeOf(&JabberData{}) { + return + } + + jabberSocket, ok := JabberSockets.Get(s.ID) + if !ok { + aid.Print("jabber socket not found even though it should be") + return + } + + aid.Print(` + `+ string(data.ToBytes()) +` + `) + + s.Connection.WriteMessage(1, []byte(` + `+ string(data.ToBytes()) +` + `)) } \ No newline at end of file diff --git a/storage/postgres.go b/storage/postgres.go index dd30de3..0846f1a 100644 --- a/storage/postgres.go +++ b/storage/postgres.go @@ -45,6 +45,7 @@ func (s *PostgresStorage) MigrateAll() { s.Migrate(&DB_PAttribute{}, "Attributes") s.Migrate(&DB_DiscordPerson{}, "Discords") s.Migrate(&DB_SeasonStat{}, "Stats") + s.Migrate(&DB_Relationship{}, "Relationships") } func (s *PostgresStorage) DropTables() { @@ -181,6 +182,26 @@ func (s *PostgresStorage) DeletePerson(personId string) { Delete(&DB_Person{}, "id = ?", personId) } +func (s *PostgresStorage) GetIncomingRelationships(personId string) []*DB_Relationship { + var dbRelationships []*DB_Relationship + s.Postgres.Model(&DB_Relationship{}).Where("incoming_person_id = ?", personId).Find(&dbRelationships) + return dbRelationships +} + +func (s *PostgresStorage) GetOutgoingRelationships(personId string) []*DB_Relationship { + var dbRelationships []*DB_Relationship + s.Postgres.Model(&DB_Relationship{}).Where("outgoing_person_id = ?", personId).Find(&dbRelationships) + return dbRelationships +} + +func (s *PostgresStorage) SaveRelationship(relationship *DB_Relationship) { + s.Postgres.Save(relationship) +} + +func (s *PostgresStorage) DeleteRelationship(relationship *DB_Relationship) { + s.Postgres.Delete(&DB_Relationship{}, "incoming_person_id = ? AND outgoing_person_id = ?", relationship.IncomingPersonID, relationship.OutgoingPersonID) +} + func (s *PostgresStorage) SaveProfile(profile *DB_Profile) { s.Postgres.Save(profile) } diff --git a/storage/storage.go b/storage/storage.go index 4fe808a..caf1364 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -9,7 +9,6 @@ type Storage interface { GetAllPersons() []*DB_Person GetPersonsCount() int - TotalVBucks() int GetPerson(personId string) *DB_Person @@ -19,6 +18,11 @@ type Storage interface { SavePerson(person *DB_Person) DeletePerson(personId string) + GetIncomingRelationships(personId string) []*DB_Relationship + GetOutgoingRelationships(personId string) []*DB_Relationship + SaveRelationship(relationship *DB_Relationship) + DeleteRelationship(relationship *DB_Relationship) + SaveProfile(profile *DB_Profile) DeleteProfile(profileId string) @@ -122,6 +126,22 @@ func (r *Repository) DeleteProfile(profileId string) { r.Storage.DeleteProfile(profileId) } +func (r *Repository) GetIncomingRelationships(personId string) []*DB_Relationship { + return r.Storage.GetIncomingRelationships(personId) +} + +func (r *Repository) GetOutgoingRelationships(personId string) []*DB_Relationship { + return r.Storage.GetOutgoingRelationships(personId) +} + +func (r *Repository) SaveRelationship(relationship *DB_Relationship) { + r.Storage.SaveRelationship(relationship) +} + +func (r *Repository) DeleteRelationship(relationship *DB_Relationship) { + r.Storage.DeleteRelationship(relationship) +} + func (r *Repository) SaveItem(item *DB_Item) { r.Storage.SaveItem(item) } diff --git a/storage/tables.go b/storage/tables.go index 4793e32..b048df3 100644 --- a/storage/tables.go +++ b/storage/tables.go @@ -20,6 +20,16 @@ func (DB_Person) TableName() string { return "Persons" } +type DB_Relationship struct { + OutgoingPersonID string `gorm:"primary_key"` + IncomingPersonID string `gorm:"primary_key"` + Status string +} + +func (DB_Relationship) TableName() string { + return "Relationships" +} + type DB_Profile struct { ID string `gorm:"primary_key"` PersonID string