From 8a8f1f98859123e162090538cbdbe7f5c9d34ea4 Mon Sep 17 00:00:00 2001 From: citrons Date: Sat, 7 Jun 2025 22:09:09 -0500 Subject: direct messages --- client/application.go | 40 +++++++++++++++++--- client/command.go | 16 ++++++++ server/channel/channel.go | 57 +++++++++++++++++++++++++--- server/server/command.go | 94 +++++++++++++++++++++++++++++++---------------- 4 files changed, 166 insertions(+), 41 deletions(-) diff --git a/client/application.go b/client/application.go index f77e0e1..e45ed5a 100644 --- a/client/application.go +++ b/client/application.go @@ -185,12 +185,21 @@ func (a *application) sendUpdate(o proto.Object, cb func(proto.Command)) { func (a *application) lookup( name string, kind string, callback func(*proto.Object, *proto.Fail)) { + a.lookupAll([]string {name}, kind, func(os []proto.Object, f *proto.Fail) { + if f == nil { + callback(&os[0], f) + } else { + callback(nil, f) + } + }) +} + +func (a *application) lookupAll(names []string, + kind string, callback func([]proto.Object, *proto.Fail)) { cb := func(response proto.Command) { switch response.Kind { case "i": - if len(response.Args) > 0 { - callback(&response.Args[0], nil) - } + callback(response.Args, nil) case "fail": if len(response.Args) > 0 { f := proto.Fail(response.Args[0]) @@ -198,8 +207,11 @@ func (a *application) lookup( } } } - o := proto.Object {kind, "", map[string]string {"": name}} - a.Request(proto.NewCmd("lookup", "", o), cb) + var os []proto.Object + for _, name := range names { + os = append(os, proto.Object {kind, "", map[string]string {"": name}}) + } + a.Request(proto.NewCmd("lookup", "", os...), cb) } func (a *application) join(channelName string) { @@ -227,6 +239,24 @@ func (a *application) join(channelName string) { }) } +func (a *application) joinDirect(among []proto.Object) { + cb := func(response proto.Command) { + switch response.Kind { + case "direct": + if len(response.Args) > 0 { + ch := response.Args[0] + a.goTo(channelLocation {id: ch.Id}) + } + case "fail": + if len(response.Args) > 0 { + f := proto.Fail(response.Args[0]) + a.cmdWindow.err(f.Error()) + } + } + } + a.Request(proto.Command{"direct", "", among}, cb) +} + func (a *application) createChannel(name string) { ch := proto.Object {"channel", "", map[string]string {"": name}} a.Request(proto.NewCmd("create", "", ch), func(response proto.Command) { diff --git a/client/command.go b/client/command.go index 2bc7868..6079070 100644 --- a/client/command.go +++ b/client/command.go @@ -117,6 +117,22 @@ func (a *application) doCommand(command string, args []string, text string) { a.cmdWindow.who(u.Id) } }) + case "msg": + a.lookup(text, "u", func(u *proto.Object, fail *proto.Fail) { + if fail != nil { + a.cmdWindow.fail(proto.Object(*fail)) + } else { + a.joinDirect([]proto.Object {*u}) + } + }) + case "msgall": + a.lookupAll(args, "u", func(us []proto.Object, fail *proto.Fail) { + if fail != nil { + a.cmdWindow.fail(proto.Object(*fail)) + } else { + a.joinDirect(us) + } + }) case "create": if a.authenticated { a.createChannel(text) diff --git a/server/channel/channel.go b/server/channel/channel.go index 449375b..056b942 100644 --- a/server/channel/channel.go +++ b/server/channel/channel.go @@ -6,11 +6,14 @@ import ( "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 } @@ -18,6 +21,7 @@ type Channel struct { store *ChannelStore id string name string + isDirect bool members map[string]Membership messages []proto.Object byId map[string]int @@ -31,7 +35,8 @@ type Tombstone struct { func NewStore(world *object.World) *ChannelStore { return &ChannelStore { - world, make(map[string]*Channel), make(map[string]Tombstone), + world, make(map[string]*Channel), make(map[string]*Channel), + make(map[string]Tombstone), } } @@ -58,12 +63,44 @@ func (cs *ChannelStore) CreateChannel(name string) (*Channel, *proto.Fail) { 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 { - return c.name + 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 { @@ -108,6 +145,9 @@ func (c *Channel) Put(m proto.Object) proto.Object { } func (c *Channel) prune() { + if c.isDirect { + return + } for m, _ := range c.members { switch c.store.world.GetObject(m).(type) { case *user.User: @@ -167,12 +207,19 @@ func (c *Channel) Delete() { c.store.world.PutObject(c.id, deleted) } -func (c *Channel) GetInfo() proto.Object { - return proto.Object { - "channel", c.id, map[string]string {"": c.name}, +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"}, diff --git a/server/server/command.go b/server/server/command.go index 306a445..14f8168 100644 --- a/server/server/command.go +++ b/server/server/command.go @@ -39,42 +39,41 @@ func (s *server) SendRequest(r session.Request) { } case "lookup": - if len(r.Cmd.Args) != 1 { - r.ReplyInvalid() - return - } - o := r.Cmd.Args[0] - var name string - for k, v := range o.Fields { - switch k { - case "": - name = v + var response []proto.Object + for _, o := range r.Cmd.Args { + var name string + for k, v := range o.Fields { + switch k { + case "": + name = v + default: + r.ReplyInvalid() + return + } + } + var info proto.Object + switch o.Kind { + case "u": + u := s.userStore.ByName(name) + if u == nil { + r.Reply(proto.Fail{"unknown-name", "", nil}.Cmd()) + return + } + info = u.GetInfo() + case "channel": + c := s.channelStore.ByName(name) + if c == nil { + r.Reply(proto.Fail{"unknown-name", "", nil}.Cmd()) + return + } + info = c.GetInfo() default: r.ReplyInvalid() return } + response = append(response, info) } - var info proto.Object - switch o.Kind { - case "u": - u := s.userStore.ByName(name) - if u == nil { - r.Reply(proto.Fail{"unknown-name", "", nil}.Cmd()) - return - } - info = u.GetInfo() - case "channel": - c := s.channelStore.ByName(name) - if c == nil { - r.Reply(proto.Fail{"unknown-name", "", nil}.Cmd()) - return - } - info = c.GetInfo() - default: - r.ReplyInvalid() - return - } - r.Reply(proto.NewCmd("i", "", info)) + r.Reply(proto.NewCmd("i", "", response...)) case "create": if r.From.UserId == "" { @@ -111,6 +110,39 @@ func (s *server) SendRequest(r session.Request) { r.ReplyInvalid() } + case "direct": + if r.From.UserId == "" { + r.ReplyInvalid() + return + } + if len(r.Cmd.Args) < 1 { + r.ReplyInvalid() + return + } + among := []string {r.From.UserId} + duplicate := make(map[string]bool) + for _, member := range r.Cmd.Args { + if member.Kind != "u" { + r.ReplyInvalid() + return + } + if duplicate[member.Fields[""]] { + r.ReplyInvalid() + return + } + duplicate[member.Fields[""]] = true + u := s.world.GetObject(member.Id) + switch u.(type) { + case *user.User: + default: + r.Reply(proto.Fail{"bad-target", "", nil}.Cmd()) + return + } + among = append(among, member.Id) + } + c := s.channelStore.GetDirect(among) + r.Reply(proto.NewCmd("direct", "", c.GetInfo())) + case "channels": if r.From.UserId == "" { r.Reply(proto.NewCmd("channels", "", )) -- cgit v1.2.3