summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcitrons <citrons@mondecitronne.com>2025-06-07 22:09:09 -0500
committercitrons <citrons@mondecitronne.com>2025-06-07 22:09:09 -0500
commit8a8f1f98859123e162090538cbdbe7f5c9d34ea4 (patch)
tree548d9ef52bfa1579c17ce1ee600ba3356fa1ffbe
parentf38d6eb807e2b921123dd5efd0b2d632a632e579 (diff)
direct messages
-rw-r--r--client/application.go40
-rw-r--r--client/command.go16
-rw-r--r--server/channel/channel.go57
-rw-r--r--server/server/command.go94
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", "", ))