package main import ( app2 "cwtch.im/cwtch/app" "cwtch.im/cwtch/event" peer2 "cwtch.im/cwtch/peer" "bytes" "cwtch.im/cwtch/model" "fmt" "git.openprivacy.ca/openprivacy/libricochet-go/connectivity" "git.openprivacy.ca/openprivacy/libricochet-go/log" "github.com/c-bata/go-prompt" "golang.org/x/crypto/ssh/terminal" "os" "os/user" "path" "strings" "syscall" "time" ) var app app2.Application var peer peer2.CwtchPeer var group *model.Group var groupFollowBreakChan chan bool var prmpt string 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-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 [groupid] [peerid]", "/new-group": "/new-group [server]", "/help": "", "/trust": "/trust [peerid]", "/block": "/block [peerid]", } func printMessage(m model.Message) { p := peer.GetContact(m.PeerID) name := "unknown" if p != nil { name = p.Name } else if peer.GetOnion() == m.PeerID { name = peer.GetName() } fmt.Printf("%v %v (%v): %v\n", m.Timestamp, name, m.PeerID, m.Message) } 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 { var s []prompt.Suggest if d.FindStartOfPreviousWord() == 0 { return prompt.FilterHasPrefix(suggestions, d.GetWordBeforeCursor(), true) } 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 } // 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 := peer.GetGroups() for _, groupID := range groups { 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) } // Suggest unaccepted group if strings.HasPrefix(w, "/accept-invite") { s = []prompt.Suggest{} 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) } // suggest groupid AND peerid if strings.HasPrefix(w, "/invite-to-group") { if d.FindStartOfPreviousWordWithSpace() == 0 { s = []prompt.Suggest{} groups := peer.GetGroups() for _, groupID := range groups { 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}) } } return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true) } s = []prompt.Suggest{} 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) } // Suggest contact onion / peerid if strings.HasPrefix(w, "/block") || strings.HasPrefix(w, "/trust") || strings.HasPrefix(w, "/invite") { s = []prompt.Suggest{} 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) } return s } func handleAppEvents(em event.Manager) { queue := event.NewQueue() em.Subscribe(event.NewPeer, queue) em.Subscribe(event.PeerError, queue) for { ev := queue.Next() switch ev.EventType { case event.NewPeer: onion := ev.Data[event.Identity] p := app.GetPeer(onion) app.LaunchPeers() fmt.Printf("\nLoaded profile %v (%v)\n", p.GetName(), p.GetOnion()) suggestions = append(suggestionsBase, suggestionsSelectedProfile...) 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") case event.PeerError: err := ev.Data[event.Error] fmt.Printf("\nError creating profile: %v\n", err) } } } func main() { cwtch := ` #, #' @@@@@@: @@@@@@. @'@@+#' @@@@+ ''''''@ #+@ : @''''+;+' . ' @''@' :+' , ; ##, +' ,@@ ;' #'#@''. #''@''# # ''''''#:,,#'''''@ : @''''@ :+'''@ ' @;+'@ @'# .:# '#..# '# @ @@@@@@ @@@@@@ '@@@@ @# . . +++, #'@+'@ ''', ''''''# .#+# ''', @'''+, @''# ''', .#@ :; '@''# .;. ''', ' : ;. , @+'''@ '+'+ @++ @+'@+''''+@ #+'''#: ''';#''+@ @@@@ @@@@@@@@@ :@@@@# #''''''# +''. +'': +'''''''''+ @'''''''# '''+'''''@ @@@@ @@@@@@@@@@@@@@@@: @'''@@'''@ @''# ,'''@ ''+ @@''+#+ :'''@@+''' ''''@@'''' @@@@ @@@@@@@@@@@@@@@@@ '''# @''# +''@ @'''# ;''@ +''+ @''@ ,+'', '''@ #'''. @@@@ @@@@ '@@@# @@@@ ;''' @@; '''# #'@'' @''@ @''+ +''# .@@ ''', '''. @@@@ @@@ @@@ .@@@ @''# #'' ''#''#@''. #''# '''. '''. +'', @@@@ @@@ @@@ @@@ @''# @''@'' #'@+'+ #''# '''. ''', +'', +@@@.@@@ @@@@ @@@, @@@ ,@@@ ;''+ @, +''@'# @'+''@ @''# +''; '+ ''', +'', @@@@@@@@# @@@@ @@@. .@@@ .@@@ '''# ++'+ ''''@ ,''''# #''' @''@ '@''+ ''', ''', @@@@@@@@: @@@@ @@@; .@@@' ;@@@ @'''@@'''@ #'''. +'''' ;'''#@ :'''#@+''+ ''', ''', @@@@@@# @@@@ @@@+ ,@@@. @@@@ #''''''# @''+ @''+ +'''' @'''''''# ''', ''', #@@@. @@@@ @@@+ @@@ @@@@ @+''+@ '++@ ;++@ '#''@ ##'''@: +++, +++, :@ @@@@ @@@' @@@ '@@@ :' ' '` fmt.Printf("%v\n\n", cwtch) quit := false usr, err := user.Current() if err != nil { log.Errorf("\nError: could not load current user: %v\n", err) os.Exit(1) } acn, err := connectivity.StartTor(path.Join(usr.HomeDir, ".cwtch"), "") if err != nil { log.Errorf("\nError connecting to Tor: %v\n", err) os.Exit(1) } app = app2.NewApp(acn, path.Join(usr.HomeDir, ".cwtch")) go handleAppEvents(app.GetPrimaryBus()) if err != nil { log.Errorf("Error initializing application: %v", err) os.Exit(1) } log.SetLevel(log.LevelDebug) 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 { prmpt = "cwtch> " if group != nil { prmpt = fmt.Sprintf("cwtch %v (%v) [%v] say> ", peer.GetName(), peer.GetOnion(), group.GroupID) } else if peer != nil { prmpt = fmt.Sprintf("cwtch %v (%v)> ", peer.GetName(), peer.GetOnion()) } text := prompt.Input(prmpt, completer, prompt.OptionSuggestionBGColor(prompt.Purple), prompt.OptionDescriptionBGColor(prompt.White), prompt.OptionPrefixTextColor(prompt.White), prompt.OptionInputTextColor(prompt.Purple), prompt.OptionHistory(history)) commands := strings.Split(text[0:], " ") history = append(history, text) 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": quit = true case "/new-profile": if len(commands) == 2 { name := strings.Trim(commands[1], " ") if name == "" { fmt.Printf("Error creating profile, usage: %v\n", usages[commands[0]]) break } fmt.Print("** WARNING: PASSWORDS CANNOT BE RECOVERED! **\n") password := "" failcount := 0 for ; failcount < 3; failcount++ { fmt.Print("Enter a password to encrypt the profile: ") bytePassword, _ := terminal.ReadPassword(int(syscall.Stdin)) if string(bytePassword) == "" { fmt.Print("\nBlank password not allowed.") continue } fmt.Print("\nRe-enter password: ") bytePassword2, _ := terminal.ReadPassword(int(syscall.Stdin)) if bytes.Equal(bytePassword, bytePassword2) { password = string(bytePassword) break } else { fmt.Print("\nPASSWORDS DIDN'T MATCH! Try again.\n") } } if failcount >= 3 { fmt.Printf("Error creating profile for %v: Your password entries must match!\n", name) } else { app.CreatePeer(name, password) } } else { fmt.Printf("Error creating New Profile, usage: %s\n", usages[commands[0]]) } case "/load-profiles": fmt.Print("Enter a password to decrypt the profile: ") bytePassword, err := terminal.ReadPassword(int(syscall.Stdin)) if err != nil { fmt.Printf("\nError loading profiles: %v\n", err) continue } app.LoadProfiles(string(bytePassword)) if err == nil { } 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 { p := app.GetPeer(commands[1]) if p == nil { fmt.Printf("Error: profile '%v' does not exist\n", commands[1]) } else { stopGroupFollow() peer = p suggestions = append(suggestionsBase, suggestionsSelectedProfile...) } // Auto cwtchPeer / Join Server // TODO There are some privacy implications with this that we should // think over. for _, name := range p.GetContacts() { profile := p.GetContact(name) if profile.Trusted && !profile.Blocked { p.PeerWithOnion(profile.Onion) } } for _, groupid := range p.GetGroups() { group := p.GetGroup(groupid) if group.Accepted || group.Owner == "self" { p.JoinServer(group.GroupServer) } } } else { fmt.Printf("Error selecting profile, usage: %s\n", usages[commands[0]]) } case "/info": if peer != nil { fmt.Printf("Address cwtch:%v\n", peer.GetOnion()) } else { fmt.Printf("Profile needs to be set\n") } case "/invite": if len(commands) == 2 { fmt.Printf("Inviting cwtch:%v\n", commands[1]) peer.PeerWithOnion(commands[1]) } else { fmt.Printf("Error inviting peer, usage: %s\n", usages[commands[0]]) } /*case "/list-peers": peers := peer.GetPeers() for p, s := range peers { fmt.Printf("Name: %v Status: %v\n", p, connections.ConnectionStateName[s]) } case "/list-servers": servers := peer.GetServers() for s, st := range servers { fmt.Printf("Name: %v Status: %v\n", s, connections.ConnectionStateName[st]) }*/ case "/list-contacts": contacts := peer.GetContacts() for _, onion := range contacts { c := peer.GetContact(onion) fmt.Printf("Name: %v Onion: %v Trusted: %v\n", c.Name, c.Onion, c.Trusted) } 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": if len(commands) == 2 { peer.TrustPeer(commands[1]) } else { fmt.Printf("Error trusting peer, usage: %s\n", usages[commands[0]]) } case "/block": if len(commands) == 2 { peer.BlockPeer(commands[1]) } else { fmt.Printf("Error blocking peer, usage: %s\n", usages[commands[0]]) } case "/accept-invite": if len(commands) == 2 { groupID := commands[1] err := peer.AcceptInvite(groupID) if err != nil { fmt.Printf("Error: %v\n", err) } else { group := peer.GetGroup(groupID) if group == nil { fmt.Printf("Error: group does not exist\n") } else { peer.JoinServer(group.GroupServer) } } } else { fmt.Printf("Error accepting invite, usage: %s\n", usages[commands[0]]) } case "/invite-to-group": if len(commands) == 3 { fmt.Printf("Inviting %v to %v\n", commands[1], commands[2]) err := peer.InviteOnionToGroup(commands[2], commands[1]) if err != nil { fmt.Printf("Error: %v\n", err) } } else { fmt.Printf("Error inviting peer to group, usage: %s\n", usages[commands[0]]) } case "/new-group": if len(commands) == 2 && commands[1] != "" { fmt.Printf("Setting up a new group on server:%v\n", commands[1]) id, _, err := peer.StartGroup(commands[1]) if err == nil { fmt.Printf("New Group [%v] created for server %v\n", id, commands[1]) group := peer.GetGroup(id) if group == nil { fmt.Printf("Error: group does not exist\n") } else { 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[commands[0]]) } case "/select-group": if len(commands) == 2 { g := peer.GetGroup(commands[1]) if g == nil { fmt.Printf("Error: group %s not found!\n", commands[1]) } else { stopGroupFollow() group = g fmt.Printf("--------------- %v ---------------\n", group.GroupID) gms := group.Timeline.GetMessages() 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 selecting a group, usage: %s\n", usages[commands[0]]) } case "/unselect-group": stopGroupFollow() case "/export-group": if len(commands) == 2 { group := peer.GetGroup(commands[1]) if group == nil { fmt.Printf("Error: group does not exist\n") } else { invite, _ := peer.ExportGroup(commands[1]) fmt.Printf("Invite: %v\n", invite) } } else { fmt.Printf("Error exporting group, usage: %s\n", usages[commands[0]]) } case "/import-group": if len(commands) == 2 { err := peer.ImportGroup(commands[1]) if err != nil { fmt.Printf("Error importing group: %v\n", err) } else { fmt.Printf("Imported group!\n") } } else { fmt.Printf("%v", commands) fmt.Printf("Error importing group, usage: %s\n", usages[commands[0]]) } case "/help": for _, command := range suggestions { fmt.Printf("%-18s%-56s%s\n", command.Text, command.Description, usages[command.Text]) } case "/sendlots": if len(commands) == 2 { 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 := 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) } } fmt.Printf("Waiting 5 seconds for message to process...\n") time.Sleep(time.Second * 5) timeline := group.GetTimeline() totalLatency := time.Duration(0) maxLatency := time.Duration(0) totalMessages := 0 for i := 0; i < 100; i++ { found := false for _, m := range timeline { if m.Message == fmt.Sprintf("this is message %v", i) && m.PeerID == peer.GetOnion() { found = true latency := m.Received.Sub(m.Timestamp) fmt.Printf("Latency for Message %v was %v\n", i, latency) totalLatency = totalLatency + latency if maxLatency < latency { maxLatency = latency } totalMessages++ } } if !found { fmt.Printf("message %v was never received\n", i) } } fmt.Printf("Average Latency for %v messages was: %vms\n", totalMessages, time.Duration(int64(totalLatency)/int64(totalMessages))) fmt.Printf("Max Latency for %v messages was: %vms\n", totalMessages, maxLatency) } } } } app.Shutdown() acn.Close() os.Exit(0) }