package channel import ( "citrons.xyz/talk/proto" "citrons.xyz/talk/server/object" "citrons.xyz/talk/server/session" "citrons.xyz/talk/server/validate" "citrons.xyz/talk/server/user" "strings" "sort" ) type ChannelStore struct { world *object.World byName map[string]*Channel directChannels map[string]*Channel deleted map[string]Tombstone } type Channel struct { store *ChannelStore id string name string isDirect bool members map[string]Membership messages []proto.Object byId map[string]int defaultMembership Membership Stream session.Stream } type Tombstone struct { name string } func NewStore(world *object.World) *ChannelStore { return &ChannelStore { world, make(map[string]*Channel), make(map[string]*Channel), make(map[string]Tombstone), } } func (cs *ChannelStore) CreateChannel(name string) (*Channel, *proto.Fail) { if cs.ByName(name) != nil { return nil, &proto.Fail { "name-taken", "", map[string]string {"": name}, } } if !validate.Name(name) { return nil, &proto.Fail { "invalid-name", "", map[string]string {"": name}, } } var c Channel c.store = cs c.name = name c.members = make(map[string]Membership) c.byId = make(map[string]int) c.defaultMembership = DefaultMembership cs.byName[validate.Fold(name)] = &c c.id = cs.world.NewObject(&c) return &c, nil } func (cs *ChannelStore) GetDirect(among []string) *Channel { sort.Strings(among) key := strings.Join(among, "\x00") if cs.directChannels[key] == nil { var c Channel c.isDirect = true c.store = cs c.byId = make(map[string]int) c.defaultMembership = DefaultMembership c.members = make(map[string]Membership) for _, member := range among { c.members[member] = c.defaultMembership } cs.directChannels[key] = &c c.id = cs.world.NewObject(&c) } return cs.directChannels[key] } func (cs *ChannelStore) ByName(name string) *Channel { return cs.byName[validate.Fold(name)] } func (c *Channel) Name() string { if !c.isDirect { return c.name } else { var members []string for member := range c.members { u := c.store.world.GetObject(member) if u != nil { members = append(members, u.GetInfo().Fields[""]) } } sort.Strings(members) return strings.Join(members, ", ") } } func (c *Channel) Id() string { return c.id } func (c *Channel) Rename(name string) *proto.Fail { if !validate.Name(name) { return &proto.Fail { "invalid-name", "", map[string]string {"": name}, } } if validate.Fold(name) == validate.Fold(c.name) { c.name = name return nil } if c.store.ByName(name) != nil { return &proto.Fail { "name-taken", "", map[string]string {"": name}, } } c.store.byName[validate.Fold(c.name)] = nil c.store.byName[validate.Fold(name)] = c c.name = name return nil } func (c *Channel) Put(m proto.Object) proto.Object { m.Id = proto.GenId() m.Fields["t"] = proto.Timestamp() c.byId[m.Id] = len(c.messages) c.messages = append(c.messages, m) for s, _ := range c.Stream.Subscribers() { if m.Fields["f"] == s.UserId { continue } if c.members[s.UserId].See { s.Event(proto.NewCmd("p", c.id, m)) } } return m } func (c *Channel) prune() { if c.isDirect { return } for m, _ := range c.members { switch c.store.world.GetObject(m).(type) { case *user.User: default: delete(c.members, m) } } } func (c *Channel) Join(u *user.User) *proto.Fail { if c.members[u.Id()].Yes { return nil } c.members[u.Id()] = c.defaultMembership u.Channels[c.id] = true c.Put(proto.Object{"join", "", map[string]string {"f": u.Id()}}) return nil } func (c *Channel) Leave(u *user.User) *proto.Fail { if !c.members[u.Id()].Yes { return nil } delete(c.members, u.Id()) delete(u.Channels, c.id) c.Put(proto.Object{"leave", "", map[string]string {"f": u.Id()}}) return nil } func (c *Channel) Members() map[string]Membership { c.prune() return c.members } func (c *Channel) SetMembership(u *user.User, m Membership) { if c.members[u.Id()].Yes { c.members[u.Id()] = m } } func (c *Channel) Delete() { c.Stream.Event(proto.NewCmd("delete", c.id)) c.Stream.UnsubscribeAll() for m, _ := range c.members { switch u := c.store.world.GetObject(m).(type) { case *user.User: u.Channels[c.id] = false default: } } delete(c.store.byName, validate.Fold(c.name)) c.store.world.RemoveObject(c.id) deleted := Tombstone {c.name} c.store.deleted[c.id] = deleted c.store.world.PutObject(c.id, deleted) } func (c *Channel) Kind() string { switch { case c.isDirect: return "direct-channel" default: return "channel" } } func (c *Channel) GetInfo() proto.Object { return proto.Object {c.Kind(), c.id, map[string]string {"": c.Name()}} } func (t Tombstone) GetInfo() proto.Object { return proto.Object { "gone", "", map[string]string {"": t.name, "kind": "channel"}, } }