diff --git a/app/app.go b/app/app.go index 406e8c2..a43e2f8 100644 --- a/app/app.go +++ b/app/app.go @@ -1,27 +1,44 @@ package app import ( + "crypto/rand" "cwtch.im/cwtch/connectivity/tor" "cwtch.im/cwtch/peer" - "errors" + "encoding/hex" "fmt" + "io/ioutil" "log" "os" "path" + "path/filepath" + "sync" ) -// Application is a facade over a cwtchPeer that provides some wrapping logic. -type Application struct { - Peer peer.CwtchPeerInterface - TorManager *tor.Manager +type application struct { + peers map[string]peer.CwtchPeer + torManager *tor.Manager directory string + mutex sync.Mutex +} + +// Application is a full cwtch peer application. It allows management, usage and storage of multiple peers +type Application interface { + LoadProfiles(password string) error + CreatePeer(name string, password string) (peer.CwtchPeer, error) + + GetPeer(onion string) peer.CwtchPeer + ListPeers() map[string]string + + Shutdown() } // NewApp creates a new app with some environment awareness and initializes a Tor Manager -func NewApp(appDirectory string, torPath string) (*Application, error) { +func NewApp(appDirectory string, torPath string) (Application, error) { log.Printf("NewApp(%v, %v)\n", appDirectory, torPath) - app := &Application{Peer: nil, directory: appDirectory} + app := &application{peers: make(map[string]peer.CwtchPeer), directory: appDirectory} os.MkdirAll(path.Join(appDirectory, "tor"), 0700) + os.Mkdir(path.Join(app.directory, "profiles"), 0700) + err := app.startTor(torPath) if err != nil { return nil, err @@ -29,22 +46,64 @@ func NewApp(appDirectory string, torPath string) (*Application, error) { return app, nil } +func generateRandomFilename() string { + randBytes := make([]byte, 16) + rand.Read(randBytes) + return filepath.Join(hex.EncodeToString(randBytes)) +} + // NewProfile creates a new cwtchPeer with a given name. -func (app *Application) NewProfile(name string, password string) error { - log.Printf("NewProfile(%v, %v)\n", name, password) - if app.Peer != nil { - return errors.New("Profile already created") +func (app *application) CreatePeer(name string, password string) (peer.CwtchPeer, error) { + log.Printf("CreatePeer(%v)\n", name) + + randomFileName := generateRandomFilename() + p := peer.NewCwtchPeer(name, password, path.Join(app.directory, "profiles", randomFileName)) + err := p.Save() + if err != nil { + p.Shutdown() //attempt + return nil, fmt.Errorf("Error attempting to save new profile: %v", err) } - app.Peer = peer.NewCwtchPeer(name, password, path.Join(app.directory, name+".json")) - err := app.Peer.Save() - if err == nil { - err = app.startPeer() + app.startPeer(p) + + _, exists := app.peers[p.GetProfile().Onion] + if exists { + p.Shutdown() + return nil, fmt.Errorf("Error: profile for onion %v already exists", p.GetProfile().Onion) } - return err + app.mutex.Lock() + app.peers[p.GetProfile().Onion] = p + app.mutex.Unlock() + + return p, nil +} + +func (app *application) LoadProfiles(password string) error { + files, err := ioutil.ReadDir(path.Join(app.directory, "profiles")) + if err != nil { + return fmt.Errorf("Error: cannot read profiles directory: %v", err) + } + + for _, file := range files { + p, err := peer.LoadCwtchPeer(path.Join(app.directory, "profiles", file.Name()), password) + if err != nil { + continue + } + _, exists := app.peers[p.GetProfile().Onion] + if exists { + p.Shutdown() + log.Printf("Error: profile for onion %v already exists", p.GetProfile().Onion) + continue + } + app.startPeer(p) + app.mutex.Lock() + app.peers[p.GetProfile().Onion] = p + app.mutex.Unlock() + } + return nil } // startTor will create a local torrc if needed -func (app *Application) startTor(torPath string) error { +func (app *application) startTor(torPath string) error { // Creating a local cwtch tor server config for the user // creating $app.directory/torrc file // SOCKSPort socksPort @@ -64,35 +123,40 @@ func (app *Application) startTor(torPath string) error { if err != nil { return err } - app.TorManager = tm + app.torManager = tm return nil } -// SetProfile loads an existing profile from the given filename. -func (app *Application) SetProfile(filename string, password string) error { - if app.Peer == nil { - profile, err := peer.LoadCwtchPeer(path.Join(app.directory, filename), password) - if err != nil { - return err - } - app.Peer = profile - return app.startPeer() - } - return errors.New("profile is already loaded, to load a different profile you will need to restart the application") -} - -func (app *Application) startPeer() error { +func (app *application) startPeer(peer peer.CwtchPeer) { go func() { - e := app.Peer.Listen() + e := peer.Listen() if e != nil { log.Panic(e) } }() +} +// ListPeers returns a map of onions to their profile's Name +func (app *application) ListPeers() map[string]string { + keys := map[string]string{} + for k, p := range app.peers { + keys[k] = p.GetProfile().Name + } + return keys +} + +// GetPeer returns a Peer for a given onion address +func (app *application) GetPeer(onion string) peer.CwtchPeer { + if peer, ok := app.peers[onion]; ok { + return peer + } return nil } -// PeerRequest attempts to setup peer relationship with the given onion address.` -func (app *Application) PeerRequest(onion string) { - app.Peer.PeerWithOnion(onion) +// Shutdown shutsdown all peers of an app and then the tormanager +func (app *application) Shutdown() { + for _, peer := range app.peers { + peer.Shutdown() + } + app.torManager.Shutdown() } diff --git a/app/cli/main.go b/app/cli/main.go index ce070c1..67cd068 100644 --- a/app/cli/main.go +++ b/app/cli/main.go @@ -2,64 +2,130 @@ package main import ( app2 "cwtch.im/cwtch/app" - "fmt" - - "github.com/c-bata/go-prompt" - "strings" - "time" + peer2 "cwtch.im/cwtch/peer" "bytes" + "cwtch.im/cwtch/model" + "cwtch.im/cwtch/peer/connections" + "fmt" + "github.com/c-bata/go-prompt" "golang.org/x/crypto/ssh/terminal" "log" "os" "os/exec" "os/user" "path" + "strings" "syscall" + "time" ) -var app *app2.Application +var app app2.Application +var peer peer2.CwtchPeer +var group *model.Group +var groupFollowBreakChan chan bool +var prmpt string -var suggestions = []prompt.Suggest{ - {Text: "new-profile", Description: "create a new profile in ~/.cwtch/$USERNAME.json"}, - {Text: "load-profile", Description: "load a new profile"}, - {Text: "quit", Description: "quit cwtch"}, - {Text: "info", Description: "show user info"}, - {Text: "servers", Description: "retrieve a list of servers and their connection status"}, - {Text: "peers", Description: "retrieve a list of peers and their connection status"}, - {Text: "contacts", Description: "retrieve a list of contacts"}, - {Text: "groups", Description: "retrieve a list of groups"}, - {Text: "export-group", Description: "export a group invite: prints as a string"}, - {Text: "send", Description: "send a message to a group"}, - {Text: "timeline", Description: "read the timeline of a given group"}, - {Text: "accept-invite", Description: "accept the invite of a group"}, - {Text: "invite", Description: "invite a new contact"}, - {Text: "invite-to-group", Description: "invite an existing contact to join an existing group"}, - {Text: "new-group", Description: "create a new group"}, - {Text: "help", Description: "print list of commands"}, - {Text: "trust", Description: "trust a peer"}, - {Text: "block", Description: "block a peer - you will no longer see messages or connect to this peer"}, +var suggestionsBase = []prompt.Suggest{ + {Text: "/new-profile", Description: "create a new profile"}, + {Text: "/load-profiles", Description: "loads profiles with a password"}, + {Text: "/list-profiles", Description: "list active profiles"}, + {Text: "/select-profile", Description: "selects an active profile to use"}, + {Text: "/help", Description: "print list of commands"}, + {Text: "/quit", Description: "quit cwtch"}, } +var suggestionsSelectedProfile = []prompt.Suggest{ + {Text: "/info", Description: "show user info"}, + {Text: "/list-contacts", Description: "retrieve a list of contacts"}, + {Text: "/list-groups", Description: "retrieve a list of groups"}, + {Text: "/new-group", Description: "create a new group on a server"}, + {Text: "/select-group", Description: "selects a group to follow"}, + {Text: "/unselect-group", Description: "stop following the current group"}, + {Text: "/invite", Description: "invite a new contact"}, + {Text: "/invite-to-group", Description: "invite an existing contact to join an existing group"}, + {Text: "/accept-invite", Description: "accept the invite of a group"}, + {Text: "/list-servers", Description: "retrieve a list of servers and their connection status"}, + {Text: "/list-peers", Description: "retrieve a list of peers and their connection status"}, + {Text: "/export-group", Description: "export a group invite: prints as a string"}, + {Text: "/trust", Description: "trust a peer"}, + {Text: "/block", Description: "block a peer - you will no longer see messages or connect to this peer"}, +} + +var suggestions = suggestionsBase + var usages = map[string]string{ - "new-profile": "new-profile [name]", - "load-profile": "load-profile [filename]", - "quit": "", - "servers": "", - "peers": "", - "contacts": "", - "groups": "", - "export-group": "export-group [groupid]", - "info": "", - "send": "send [groupid] [message]", - "timeline": "timeline [groupid]", - "accept-invite": "accept-invite [groupid]", - "invite": "invite [peerid]", - "invite-to-group": "invite-to-group [peerid] [groupid]", - "new-group": "new-group [server]", - "help": "", - "trust": "trust [peerid]", - "block": "block [peerid]", + "/new-profile": "/new-profile [name]", + "/load-profiles": "/load-profiles", + "/list-profiles": "", + "/select-profile": "/select-profile [onion]", + "/quit": "", + "/list-servers": "", + "/list-peers": "", + "/list-contacts": "", + "/list-groups": "", + "/select-group": "/select-group [groupid]", + "/unselect-group": "", + "/export-group": "/export-group [groupid]", + "/info": "", + "/send": "/send [groupid] [message]", + "/timeline": "/timeline [groupid]", + "/accept-invite": "/accept-invite [groupid]", + "/invite": "/invite [peerid]", + "/invite-to-group": "/invite-to-group [peerid] [groupid]", + "/new-group": "/new-group [server]", + "/help": "", + "/trust": "/trust [peerid]", + "/block": "/block [peerid]", +} + +func printMessage(m model.Message) { + verified := "not-verified" + if m.Verified { + verified = "verified" + } + + p := peer.GetContact(m.PeerID) + name := "unknown" + if p != nil { + name = p.Name + } else if peer.GetProfile().Onion == m.PeerID { + name = peer.GetProfile().Name + } + + fmt.Printf("%v %v (%v): %v [%s]\n", m.Timestamp, name, m.PeerID, m.Message, verified) +} + +func startGroupFollow() { + groupFollowBreakChan = make(chan bool) + go func() { + for { + l := len(group.Timeline.GetMessages()) + select { + case <-time.After(1 * time.Second): + if group == nil { + return + } + gms := group.Timeline.GetMessages() + if len(gms) != l { + fmt.Printf("\n") + for ; l < len(gms); l++ { + printMessage(gms[l]) + } + fmt.Printf(prmpt) + } + case <-groupFollowBreakChan: + return + } + } + }() +} + +func stopGroupFollow() { + if group != nil { + groupFollowBreakChan <- true + group = nil + } } func completer(d prompt.Document) []prompt.Suggest { @@ -70,46 +136,62 @@ func completer(d prompt.Document) []prompt.Suggest { return prompt.FilterHasPrefix(suggestions, d.GetWordBeforeCursor(), true) } - if app.Peer == nil { + w := d.CurrentLine() + + // Suggest a profile id + if strings.HasPrefix(w, "/select-profile") { + s = []prompt.Suggest{} + peerlist := app.ListPeers() + for onion, peername := range peerlist { + s = append(s, prompt.Suggest{Text: onion, Description: peername}) + } + } + + if peer == nil { return s } - w := d.CurrentLine() - if strings.HasPrefix(w, "send") || strings.HasPrefix(w, "timeline") || strings.HasPrefix(w, "export-group") { + // Suggest groupid + if /*strings.HasPrefix(w, "send") || strings.HasPrefix(w, "timeline") ||*/ strings.HasPrefix(w, "/export-group") || strings.HasPrefix(w, "/select-group") { s = []prompt.Suggest{} - groups := app.Peer.GetGroups() + groups := peer.GetGroups() for _, groupID := range groups { - group := app.Peer.GetGroup(groupID) + group := peer.GetGroup(groupID) s = append(s, prompt.Suggest{Text: group.GroupID, Description: "Group owned by " + group.Owner + " on " + group.GroupServer}) } return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true) } - if strings.HasPrefix(w, "block") || strings.HasPrefix(w, "trust") { + // Suggest unaccepted group + if strings.HasPrefix(w, "/accept-invite") { s = []prompt.Suggest{} - contacts := app.Peer.GetContacts() - for _, onion := range contacts { - contact := app.Peer.GetContact(onion) - s = append(s, prompt.Suggest{Text: contact.Onion, Description: contact.Name}) + groups := peer.GetGroups() + for _, groupID := range groups { + group := peer.GetGroup(groupID) + if group.Accepted == false { + s = append(s, prompt.Suggest{Text: group.GroupID, Description: "Group owned by " + group.Owner + " on " + group.GroupServer}) + } } return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true) } - if strings.HasPrefix(w, "invite-to-group") { + // suggest peerid AND groupid + if strings.HasPrefix(w, "/invite-to-group") { if d.FindStartOfPreviousWordWithSpace() == 0 { s = []prompt.Suggest{} - contacts := app.Peer.GetContacts() + contacts := peer.GetContacts() for _, onion := range contacts { - contact := app.Peer.GetContact(onion) + contact := peer.GetContact(onion) s = append(s, prompt.Suggest{Text: contact.Onion, Description: contact.Name}) } return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true) } + s = []prompt.Suggest{} - groups := app.Peer.GetGroups() + groups := peer.GetGroups() for _, groupID := range groups { - group := app.Peer.GetGroup(groupID) + group := peer.GetGroup(groupID) if group.Owner == "self" { s = append(s, prompt.Suggest{Text: group.GroupID, Description: "Group owned by " + group.Owner + " on " + group.GroupServer}) } @@ -117,14 +199,13 @@ func completer(d prompt.Document) []prompt.Suggest { return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true) } - if strings.HasPrefix(w, "accept-invite") { + // Suggest contact onion / peerid + if strings.HasPrefix(w, "/block") || strings.HasPrefix(w, "/trust") || strings.HasPrefix(w, "/invite") { s = []prompt.Suggest{} - groups := app.Peer.GetGroups() - for _, groupID := range groups { - group := app.Peer.GetGroup(groupID) - if group.Accepted == false { - s = append(s, prompt.Suggest{Text: group.GroupID, Description: "Group owned by " + group.Owner + " on " + group.GroupServer}) - } + contacts := peer.GetContacts() + for _, onion := range contacts { + contact := peer.GetContact(onion) + s = append(s, prompt.Suggest{Text: contact.Onion, Description: contact.Name}) } return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true) } @@ -188,13 +269,21 @@ func main() { if err != nil { log.Fatalf("Error initializing application: %v", err) } + fmt.Printf("\nWelcome to Cwtch!\n") + fmt.Printf("If this if your first time you should create a profile by running `/new-profile`\n") + fmt.Printf("`/load-profiles` will prompt you for a password and load profiles from storage\n") + fmt.Printf("`/help` will show you other available commands\n") + fmt.Printf("There is full [TAB] completion support\n\n") + var history []string for !quit { - profile := "unset" - if app.Peer != nil { - profile = app.Peer.GetProfile().Name + + prmpt = "cwtch> " + if group != nil { + prmpt = fmt.Sprintf("cwtch %v (%v) [%v] say> ", peer.GetProfile().Name, peer.GetProfile().Onion, group.GroupID) + } else if peer != nil { + prmpt = fmt.Sprintf("cwtch %v (%v)> ", peer.GetProfile().Name, peer.GetProfile().Onion) } - prmpt := fmt.Sprintf("cwtch [%v]> ", profile) text := prompt.Input(prmpt, completer, prompt.OptionSuggestionBGColor(prompt.Purple), prompt.OptionDescriptionBGColor(prompt.White), @@ -203,20 +292,28 @@ func main() { commands := strings.Split(text[0:], " ") history = append(history, text) - if app.Peer == nil { - if commands[0] != "help" && commands[0] != "quit" && commands[0] != "new-profile" && commands[0] != "load-profile" { + if peer == nil { + if commands[0] != "/help" && commands[0] != "/quit" && commands[0] != "/new-profile" && commands[0] != "/load-profiles" && commands[0] != "/select-profile" && commands[0] != "/list-profiles" { fmt.Printf("Profile needs to be set\n") continue } } + // Send + if group != nil && !strings.HasPrefix(commands[0], "/") { + err := peer.SendMessageToGroup(group.GroupID, text) + if err != nil { + fmt.Printf("Error sending message: %v\n", err) + } + } + switch commands[0] { - case "quit": - if app.Peer != nil { - app.Peer.Save() + case "/quit": + if peer != nil { + peer.Save() } quit = true - case "new-profile": + case "/new-profile": if len(commands) == 2 { fmt.Print("** WARNING: PASSWORDS CANNOT BE RECOVERED! **\n") @@ -242,187 +339,197 @@ func main() { if failcount >= 3 { fmt.Printf("Error creating profile for %v: Your password entries must match!\n", commands[1]) } else { - err := app.NewProfile(commands[1], password) + p, err := app.CreatePeer(commands[1], password) if err == nil { + stopGroupFollow() fmt.Printf("\nNew profile created for %v\n", commands[1]) + peer = p + suggestions = append(suggestionsBase, suggestionsSelectedProfile...) + } else { fmt.Printf("\nError creating profile for %v: %v\n", commands[1], err) } } } else { - fmt.Printf("Error creating NewProfile, usage: %s\n", usages["new-profile"]) + fmt.Printf("Error creating New Profile, usage: %s\n", usages[commands[0]]) } - case "load-profile": + case "/load-profiles": + fmt.Print("Enter a password to decrypt the profile: ") + bytePassword, err := terminal.ReadPassword(int(syscall.Stdin)) + + err = app.LoadProfiles(string(bytePassword)) + if err == nil { + profiles := app.ListPeers() + fmt.Printf("\n%v profiles active now\n", len(profiles)) + fmt.Printf("You should run `select-profile` to use a profile or `list-profiles` to view loaded profiles\n") + } else { + fmt.Printf("\nError loading profiles: %v\n", err) + } + case "/list-profiles": + peerlist := app.ListPeers() + for onion, peername := range peerlist { + fmt.Printf(" %v\t%v\n", onion, peername) + } + case "/select-profile": if len(commands) == 2 { - fmt.Print("Enter a password to decrypt the profile: ") - bytePassword, err := terminal.ReadPassword(int(syscall.Stdin)) - err = app.SetProfile(commands[1]+".json", string(bytePassword)) - if err == nil { - fmt.Printf("\nLoaded profile for %v\n", commands[1]) + p := app.GetPeer(commands[1]) + if p == nil { + fmt.Printf("Error: profile '%v' does not exist\n", commands[1]) } else { - fmt.Printf("\nError loading profile for %v: %v\n", commands[1], err) + stopGroupFollow() + peer = p + suggestions = append(suggestionsBase, suggestionsSelectedProfile...) } } else { - fmt.Printf("Error Loading profile, usage: %s\n", usages["load-profile"]) + fmt.Printf("Error selecting profile, usage: %s\n", usages[commands[0]]) } - - case "info": - if app.Peer != nil { - fmt.Printf("Address cwtch:%v\n", app.Peer.GetProfile().Onion) + case "/info": + if peer != nil { + fmt.Printf("Address cwtch:%v\n", peer.GetProfile().Onion) } else { fmt.Printf("Profile needs to be set\n") } - case "invite": + case "/invite": if len(commands) == 2 { fmt.Printf("Inviting cwtch:%v\n", commands[1]) - app.PeerRequest(commands[1]) + peer.PeerWithOnion(commands[1]) } else { - fmt.Printf("Error inviting peer, usage: %s\n", usages["invite"]) + fmt.Printf("Error inviting peer, usage: %s\n", usages[commands[0]]) } - case "peers": - peers := app.Peer.GetPeers() + case "/list-peers": + peers := peer.GetPeers() for p, s := range peers { - fmt.Printf("Name: %v Status: %v\n", p, s) + fmt.Printf("Name: %v Status: %v\n", p, connections.ConnectionStateName[s]) } - case "servers": - servers := app.Peer.GetServers() + case "/list-servers": + servers := peer.GetServers() for s, st := range servers { fmt.Printf("Name: %v Status: %v\n", s, st) } - case "contacts": - contacts := app.Peer.GetContacts() + case "/list-contacts": + contacts := peer.GetContacts() for _, onion := range contacts { - c := app.Peer.GetContact(onion) + c := peer.GetContact(onion) fmt.Printf("Name: %v Onion: %v Trusted: %v\n", c.Name, c.Onion, c.Trusted) } - case "groups": - for _, gid := range app.Peer.GetGroups() { - g := app.Peer.GetGroup(gid) + case "/list-groups": + for _, gid := range peer.GetGroups() { + g := peer.GetGroup(gid) fmt.Printf("Group Id: %v Owner: %v Accepted:%v\n", gid, g.Owner, g.Accepted) } - case "trust": + case "/trust": if len(commands) == 2 { - app.Peer.TrustPeer(commands[1]) + peer.TrustPeer(commands[1]) } else { - fmt.Printf("Error trusting peer, usage: %s\n", usages["trust"]) + fmt.Printf("Error trusting peer, usage: %s\n", usages[commands[0]]) } - case "block": + case "/block": if len(commands) == 2 { - app.Peer.BlockPeer(commands[1]) + peer.BlockPeer(commands[1]) } else { - fmt.Printf("Error blocking peer, usage: %s\n", usages["trust"]) + fmt.Printf("Error blocking peer, usage: %s\n", usages[commands[0]]) } - case "accept-invite": + case "/accept-invite": if len(commands) == 2 { groupID := commands[1] - err := app.Peer.AcceptInvite(groupID) + err := peer.AcceptInvite(groupID) if err != nil { fmt.Printf("Error: %v\n", err) } else { - app.Peer.Save() - group := app.Peer.GetGroup(groupID) + peer.Save() + group := peer.GetGroup(groupID) if group == nil { fmt.Printf("Error: group does not exist\n") } else { - app.Peer.JoinServer(group.GroupServer) + peer.JoinServer(group.GroupServer) } } } else { - fmt.Printf("Error accepting invite, usage: %s\n", usages["accept-invite"]) + fmt.Printf("Error accepting invite, usage: %s\n", usages[commands[0]]) } - case "invite-to-group": + case "/invite-to-group": if len(commands) == 3 { fmt.Printf("Inviting %v to %v\n", commands[1], commands[2]) - err := app.Peer.InviteOnionToGroup(commands[1], commands[2]) + err := peer.InviteOnionToGroup(commands[1], commands[2]) if err != nil { fmt.Printf("Error: %v\n", err) } } else { - fmt.Printf("Error inviting peer to group, usage: %s\n", usages["invite-to-group"]) + fmt.Printf("Error inviting peer to group, usage: %s\n", usages[commands[0]]) } - case "new-group": + case "/new-group": if len(commands) == 2 && commands[1] != "" { fmt.Printf("Setting up a new group on server:%v\n", commands[1]) - id, _, err := app.Peer.StartGroup(commands[1]) + id, _, err := peer.StartGroup(commands[1]) if err == nil { fmt.Printf("New Group [%v] created for server %v\n", id, commands[1]) - app.Peer.Save() - group := app.Peer.GetGroup(id) + peer.Save() + group := peer.GetGroup(id) if group == nil { fmt.Printf("Error: group does not exist\n") } else { - app.Peer.JoinServer(group.GroupServer) + peer.JoinServer(group.GroupServer) } } else { fmt.Printf("Error creating new group: %v", err) } } else { - fmt.Printf("Error creating a new group, usage: %s\n", usages["new-group"]) + fmt.Printf("Error creating a new group, usage: %s\n", usages[commands[0]]) } - case "send": - if len(commands) > 2 { - message := strings.Join(commands[2:], " ") - err := app.Peer.SendMessageToGroup(commands[1], message) - if err != nil { - fmt.Printf("Error: %v\n", err) - } - } else { - fmt.Printf("Error sending message to group, usage: %s\n", usages["send"]) - } - case "timeline": + case "/select-group": if len(commands) == 2 { - group := app.Peer.GetGroup(commands[1]) - if group == nil { - fmt.Printf("Error: group does not exist\n") + g := peer.GetGroup(commands[1]) + if g == nil { + fmt.Printf("Error: group %s not found!\n", commands[1]) } else { - timeline := group.GetTimeline() - for _, m := range timeline { - verified := "not-verified" - if m.Verified { - verified = "verified" - } + stopGroupFollow() + group = g - p := app.Peer.GetContact(m.PeerID) - name := "unknown" - if p != nil { - name = p.Name - } else if app.Peer.GetProfile().Onion == m.PeerID { - name = app.Peer.GetProfile().Name - } - - fmt.Printf("%v %v (%v): %v [%s]\n", m.Timestamp, name, m.PeerID, m.Message, verified) + fmt.Printf("--------------- %v ---------------\n", group.GroupID) + gms := group.Timeline.Messages + max := 20 + if len(gms) < max { + max = len(gms) } + for i := len(gms) - max; i < len(gms); i++ { + printMessage(gms[i]) + } + fmt.Printf("------------------------------\n") + + startGroupFollow() } } else { - fmt.Printf("Error reading timeline from group, usage: %s\n", usages["timeline"]) + fmt.Printf("Error selecting a group, usage: %s\n", usages[commands[0]]) } - case "export-group": + case "/unselect-group": + stopGroupFollow() + case "/export-group": if len(commands) == 2 { - group := app.Peer.GetGroup(commands[1]) + group := peer.GetGroup(commands[1]) if group == nil { fmt.Printf("Error: group does not exist\n") } else { - invite, _ := app.Peer.ExportGroup(commands[1]) + invite, _ := peer.ExportGroup(commands[1]) fmt.Printf("Invite: %v\n", invite) } } else { - fmt.Printf("Error exporting group, usage: %s\n", usages["export-group"]) + fmt.Printf("Error exporting group, usage: %s\n", usages[commands[0]]) } - case "save": - app.Peer.Save() - case "help": + case "/save": + peer.Save() + case "/help": for _, command := range suggestions { fmt.Printf("%-18s%-56s%s\n", command.Text, command.Description, usages[command.Text]) } - case "sendlots": + case "/sendlots": if len(commands) == 2 { - group := app.Peer.GetGroup(commands[1]) + group := peer.GetGroup(commands[1]) if group == nil { fmt.Printf("Error: group does not exist\n") } else { for i := 0; i < 100; i++ { fmt.Printf("Sending message: %v\n", i) - err := app.Peer.SendMessageToGroup(commands[1], fmt.Sprintf("this is message %v", i)) + err := peer.SendMessageToGroup(commands[1], fmt.Sprintf("this is message %v", i)) if err != nil { fmt.Printf("could not send message %v because %v\n", i, err) } @@ -436,7 +543,7 @@ func main() { for i := 0; i < 100; i++ { found := false for _, m := range timeline { - if m.Message == fmt.Sprintf("this is message %v", i) && m.PeerID == app.Peer.GetProfile().Onion { + if m.Message == fmt.Sprintf("this is message %v", i) && m.PeerID == peer.GetProfile().Onion { found = true latency := m.Received.Sub(m.Timestamp) fmt.Printf("Latency for Message %v was %v\n", i, latency) @@ -459,15 +566,12 @@ func main() { } } } - if app.Peer != nil { - app.Peer.Save() + if peer != nil { + peer.Save() } } - if app.TorManager != nil { - fmt.Println("Shutting down Tor process...") - app.TorManager.Shutdown() - } + app.Shutdown() os.Exit(0) } diff --git a/peer/connections/state.go b/peer/connections/state.go index e8c9cb0..c71fbad 100644 --- a/peer/connections/state.go +++ b/peer/connections/state.go @@ -16,3 +16,8 @@ const ( FAILED KILLED ) + +var ( + // ConnectionStateName allows conversaion of states to their string representations + ConnectionStateName = []string{"Disconnected", "Connecting", "Connected", "Authenticated", "Failed", "Killed"} +) diff --git a/peer/cwtch_peer.go b/peer/cwtch_peer.go index 3c8f8e8..29051ec 100644 --- a/peer/cwtch_peer.go +++ b/peer/cwtch_peer.go @@ -40,9 +40,9 @@ type cwtchPeer struct { salt [128]byte } -// CwtchPeerInterface provides us with a way of testing systems built on top of cwtch without having to +// CwtchPeer provides us with a way of testing systems built on top of cwtch without having to // directly implement a cwtchPeer. -type CwtchPeerInterface interface { +type CwtchPeer interface { Save() error PeerWithOnion(string) InviteOnionToGroup(string, string) error @@ -145,7 +145,7 @@ func (cp *cwtchPeer) setup() { } // NewCwtchPeer creates and returns a new cwtchPeer with the given name. -func NewCwtchPeer(name string, password string, profilefile string) CwtchPeerInterface { +func NewCwtchPeer(name string, password string, profilefile string) CwtchPeer { cp := new(cwtchPeer) cp.profilefile = profilefile cp.Profile = model.GenerateNewProfile(name) @@ -169,7 +169,7 @@ func (cp *cwtchPeer) Save() error { } // LoadCwtchPeer loads an existing cwtchPeer from a file. -func LoadCwtchPeer(profilefile string, password string) (CwtchPeerInterface, error) { +func LoadCwtchPeer(profilefile string, password string) (CwtchPeer, error) { encryptedbytes, err := ioutil.ReadFile(profilefile) if err == nil { var dkr [32]byte @@ -410,6 +410,7 @@ func (cp *cwtchPeer) Listen() error { func (cp *cwtchPeer) Shutdown() { cp.connectionsManager.Shutdown() cp.app.Shutdown() + cp.Save() } // CwtchPeerInstance encapsulates incoming peer connections diff --git a/testing/cwtch_peer_server_intergration_test.go b/testing/cwtch_peer_server_intergration_test.go index bb9d5cd..03e4cbd 100644 --- a/testing/cwtch_peer_server_intergration_test.go +++ b/testing/cwtch_peer_server_intergration_test.go @@ -84,7 +84,7 @@ func serverCheck(t *testing.T, serverAddr string) bool { return true } -func waitForPeerConnection(t *testing.T, peer peer.CwtchPeerInterface, server string) { +func waitForPeerConnection(t *testing.T, peer peer.CwtchPeer, server string) { for { servers := peer.GetServers() state, ok := servers[server]