Skip to content

Commit

Permalink
Merge pull request #91 from arborchat/presence
Browse files Browse the repository at this point in the history
Darklaunch session tracking
  • Loading branch information
whereswaldon authored Jan 26, 2019
2 parents 611790b + 8174e50 commit 11718b7
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 35 deletions.
24 changes: 12 additions & 12 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

97 changes: 96 additions & 1 deletion client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ import (
"log"
"net"
"runtime"
"strconv"
"strings"
"time"

arbor "github.com/arborchat/arbor-go"
"github.com/arborchat/muscadine/archive"
"github.com/arborchat/muscadine/session"
"github.com/arborchat/muscadine/types"
uuid "github.com/nu7hatch/gouuid"
)

const timeout = 30 * time.Second
Expand All @@ -34,7 +38,9 @@ type NetClient struct {
address string

arbor.ReadWriteCloser
connectFunc Connector
connectFunc Connector
*session.List
session.Session
disconnectHandler func(types.Connection)
receiveHandler func(*arbor.ChatMessage)
stopSending chan struct{}
Expand All @@ -57,13 +63,20 @@ func NewNetClient(address, username string, history *archive.Manager) (*NetClien
composerOut := make(chan *arbor.ProtocolMessage)
stopSending := make(chan struct{})
stopReceiving := make(chan struct{})

sessionID, err := uuid.NewV4()
if err != nil {
return nil, fmt.Errorf("Couldn't generate session id: %s", err)
}
nc := &NetClient{
address: address,
Manager: history,
connectFunc: TCPDial,
Composer: Composer{username: username, sendChan: composerOut},
stopSending: stopSending,
stopReceiving: stopReceiving,
List: session.NewList(),
Session: session.Session{ID: sessionID.String()},
pingServer: make(chan struct{}),
}
return nc, nil
Expand Down Expand Up @@ -137,6 +150,7 @@ func (nc *NetClient) send() {
// query for the root message
root, _ := nc.Archive.Root()
go nc.Composer.Query(root)
go nc.Composer.AskWho()
case <-nc.stopSending:
return
}
Expand Down Expand Up @@ -201,6 +215,87 @@ func (nc *NetClient) handleMessage(m *arbor.ProtocolMessage) {
nc.Query(recent)
}
}
case arbor.MetaType:
nc.HandleMeta(m.Meta)
}
}

// SessionID returns the unique identifier for this session.
func (nc *NetClient) SessionID() string {
return nc.Session.ID
}

// parsePresence processes "presence/here" META values into their constituent parts.
// These values take the form "username\nsessionID\ntimestamp", where username is the user
// who is advertising their presence, sessionID is the unique identifier for their session,
// and timestamp is the UNIX epoch time at which they announced their presence.
func parsePresence(value string) (username, sessionID string, timestamp time.Time, err error) {
parts := strings.Split(value, "\n")
if len(parts) < 3 {
err = fmt.Errorf("invalid presence/here message: %s", value)
return
}
username = parts[0]
if username == "" {
err = fmt.Errorf("Username cannot be the empty string")
return
}
sessionID = parts[1]
if sessionID == "" {
err = fmt.Errorf("SessionID cannot be the empty string")
return
}
timeString := parts[2]
timeInt, err := strconv.Atoi(timeString)
if err != nil {
err = fmt.Errorf("Error decoding timestamp in presence/here message: %s", value)
return
}
timestamp = time.Unix(int64(timeInt), 0)
return
}

// HandleMeta implements META message protocol extension handlers.
func (nc *NetClient) HandleMeta(meta map[string]string) {
for key, value := range meta {
switch key {
case "presence/who":
nc.Composer.AnnounceHere(nc.Session.ID)
case "presence/here":
username, sessionID, timestamp, err := parsePresence(value)
if err != nil {
log.Println("error parsing presence/here message", err)
continue
}
if username == nc.username && sessionID == nc.Session.ID {
// don't track our own session
continue
}
err = nc.List.Track(username, session.Session{ID: sessionID, LastSeen: timestamp})
if err != nil {
log.Println("Error updating session", err)
continue
}
log.Printf("Tracking session (id=%s) for user %s\n", sessionID, username)
case "presence/leave":
username, sessionID, _, err := parsePresence(value)
if err != nil {
log.Println("error parsing presence/leave message", err)
continue
}
if username == nc.username && sessionID == nc.Session.ID {
// don't remove our own session
continue
}
err = nc.List.Remove(username, sessionID)
if err != nil {
log.Println("Error removing session", err)
continue
}
log.Printf("Removed session (id=%s) for user %s\n", sessionID, username)
default:
log.Println("Unknown meta key:", key)
}
}
}

Expand Down
33 changes: 33 additions & 0 deletions composer.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package main

import (
"fmt"
"time"

arbor "github.com/arborchat/arbor-go"
)

Expand All @@ -27,3 +30,33 @@ func (c *Composer) Reply(parent, content string) error {
func (c *Composer) Query(id string) {
c.sendChan <- &arbor.ProtocolMessage{Type: arbor.QueryType, ChatMessage: &arbor.ChatMessage{UUID: id}}
}

// AnnounceHere sends a "presence/here" META message.
func (c *Composer) AnnounceHere(sessionID string) {
c.sendChan <- &arbor.ProtocolMessage{
Type: arbor.MetaType,
Meta: map[string]string{
"presence/here": c.username + "\n" + sessionID + "\n" + fmt.Sprintf("%d", time.Now().Unix()),
},
}
}

// AnnounceLeaving sends a "presence/leave" META message.
func (c *Composer) AnnounceLeaving(sessionID string) {
c.sendChan <- &arbor.ProtocolMessage{
Type: arbor.MetaType,
Meta: map[string]string{
"presence/leave": c.username + "\n" + sessionID + "\n" + fmt.Sprintf("%d", time.Now().Unix()),
},
}
}

// AskWho sends a "presence/who" META message.
func (c *Composer) AskWho() {
c.sendChan <- &arbor.ProtocolMessage{
Type: arbor.MetaType,
Meta: map[string]string{
"presence/who": "",
},
}
}
4 changes: 2 additions & 2 deletions session/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ func NewList() *List {
return &List{make(map[string]map[string]time.Time)}
}

// Add updates the List with the given session information for the given
// Track updates the List with the given session information for the given
// user. If the user has a session with the same ID already, the LastSeen
// time is updated to reflect the LastSeen time in sess.
func (l *List) Add(username string, sess Session) error {
func (l *List) Track(username string, sess Session) error {
if username == "" || sess.ID == "" {
return fmt.Errorf("Invalid username (%s) or session ID (%s)", username, sess.ID)
}
Expand Down
30 changes: 15 additions & 15 deletions session/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,34 +20,34 @@ func TestCreateUserList(t *testing.T) {
g.Expect(list).ToNot(gomega.BeNil())
}

// TestAddSession ensures that adding a valid session succeeds
func TestAddSession(t *testing.T) {
// TestTrackSession ensures that adding a valid session succeeds
func TestTrackSession(t *testing.T) {
g := gomega.NewGomegaWithT(t)
list := session.NewList()
err := list.Add(username, session.Session{ID: sessionName, LastSeen: time.Now()})
err := list.Track(username, session.Session{ID: sessionName, LastSeen: time.Now()})
g.Expect(err).To(gomega.BeNil())
// adding the same session twice should not err
err = list.Add(username, session.Session{ID: sessionName, LastSeen: time.Now()})
err = list.Track(username, session.Session{ID: sessionName, LastSeen: time.Now()})
g.Expect(err).To(gomega.BeNil())
}

// TestAddBadSession ensures that adding an invalid session succeeds
func TestAddBadSession(t *testing.T) {
// TestTrackBadSession ensures that adding an invalid session succeeds
func TestTrackBadSession(t *testing.T) {
g := gomega.NewGomegaWithT(t)
list := session.NewList()
err := list.Add("", session.Session{sessionName, time.Now()})
err := list.Track("", session.Session{sessionName, time.Now()})
g.Expect(err).ToNot(gomega.BeNil())
err = list.Add(username, session.Session{})
err = list.Track(username, session.Session{})
g.Expect(err).ToNot(gomega.BeNil())
}

// TestRemoveSession ensures that removing a real session succeeds
func TestRemoveSession(t *testing.T) {
g := gomega.NewGomegaWithT(t)
list := session.NewList()
err := list.Add(username, session.Session{ID: sessionName, LastSeen: time.Now()})
err := list.Track(username, session.Session{ID: sessionName, LastSeen: time.Now()})
if err != nil {
t.Skip("Adding failed", err)
t.Skip("Tracking failed", err)
}
err = list.Remove(username, sessionName)
g.Expect(err).To(gomega.BeNil())
Expand Down Expand Up @@ -80,7 +80,7 @@ func TestRemoveInvalidSession(t *testing.T) {
func TestActiveSessions(t *testing.T) {
g := gomega.NewGomegaWithT(t)
list := session.NewList()
_ = list.Add(username, session.Session{sessionName, time.Now()})
_ = list.Track(username, session.Session{sessionName, time.Now()})
active := list.ActiveSessions()
g.Expect(active).ToNot(gomega.BeNil())
g.Expect(len(active)).To(gomega.BeEquivalentTo(1))
Expand All @@ -93,8 +93,8 @@ func TestActiveMultiSessions(t *testing.T) {
g := gomega.NewGomegaWithT(t)
list := session.NewList()
secondName := sessionName + "-second"
_ = list.Add(username, session.Session{sessionName, time.Now().Add(-1 * time.Second)})
_ = list.Add(username, session.Session{secondName, time.Now()})
_ = list.Track(username, session.Session{sessionName, time.Now().Add(-1 * time.Second)})
_ = list.Track(username, session.Session{secondName, time.Now()})
// multiple sessions for the same user should still result in a single result
active := list.ActiveSessions()
g.Expect(active).ToNot(gomega.BeNil())
Expand All @@ -113,8 +113,8 @@ func TestActiveMultiUserSessions(t *testing.T) {
g := gomega.NewGomegaWithT(t)
list := session.NewList()
secondUser := username + "-second"
_ = list.Add(username, session.Session{sessionName, time.Now()})
_ = list.Add(secondUser, session.Session{sessionName, time.Now()})
_ = list.Track(username, session.Session{sessionName, time.Now()})
_ = list.Track(secondUser, session.Session{sessionName, time.Now()})
// multiple sessions for the same user should still result in a single result
active := list.ActiveSessions()
g.Expect(active).ToNot(gomega.BeNil())
Expand Down
12 changes: 7 additions & 5 deletions tui/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type TUI struct {
*gocui.Gui
done chan struct{}
messages chan *arbor.ChatMessage
types.Composer
types.Client
*Editor
histState *HistoryState
init sync.Once
Expand All @@ -47,7 +47,7 @@ func NewTUI(client types.Client) (*TUI, error) {
Gui: gui,
messages: make(chan *arbor.ChatMessage),
histState: hs,
Composer: client,
Client: client,
Editor: NewEditor(),
}
client.OnReceive(t.Display)
Expand Down Expand Up @@ -76,9 +76,10 @@ func (t *TUI) manageConnection(c types.Connection) {
}
log.Println("Connected to server")
go func() {
t.Client.AnnounceHere(t.SessionID())
for i := 0; i < 5; i++ {
if root, err := t.histState.Root(); err == nil {
t.Composer.Reply(root, "[join]")
t.Client.Reply(root, "[join]")
return
}
time.Sleep(5 * time.Second)
Expand Down Expand Up @@ -157,10 +158,11 @@ func (t *TUI) Display(message *arbor.ChatMessage) {
// quit asks the TUI to stop running. Should only be called as
// a keystroke or mouse input handler.
func (t *TUI) quit(c *gocui.Gui, v *gocui.View) error {
t.Client.AnnounceLeaving(t.SessionID())
if root, err := t.histState.Root(); err != nil {
log.Println("Not notifying that we quit:", err)
} else {
t.Composer.Reply(root, "[quit]")
t.Client.Reply(root, "[quit]")
time.Sleep(time.Millisecond * 250) // wait in the hope that Quit will be sent
}
return gocui.ErrQuit
Expand Down Expand Up @@ -341,7 +343,7 @@ func (t *TUI) sendReply(c *gocui.Gui, v *gocui.View) error {
if content[len(content)-1] == '\n' {
content = content[:len(content)-1]
}
t.Composer.Reply(t.Editor.ReplyTo.UUID, content)
t.Client.Reply(t.Editor.ReplyTo.UUID, content)
return t.historyMode()
}

Expand Down
Loading

0 comments on commit 11718b7

Please sign in to comment.