package main import ( libpeer "cwtch.im/cwtch/peer" "cwtch.im/cwtch/peer/connections" "encoding/base32" "fmt" "github.com/sethvargo/go-diceware/diceware" "github.com/therecipe/qt/core" "github.com/therecipe/qt/quick" "github.com/therecipe/qt/quickcontrols2" "github.com/therecipe/qt/widgets" "os" "os/user" "path" "strings" "time" "strconv" "git.openprivacy.ca/openprivacy/asaur" "git.openprivacy.ca/openprivacy/libricochet-go/application" "git.openprivacy.ca/openprivacy/libricochet-go/channels" "log" "cwtch.im/cwtch/model" "encoding/hex" "cwtch.im/cwtch/app" ) var gcd *GrandCentralDispatcher type ContactManager map[string]*Contact var contactMgr ContactManager var peer libpeer.CwtchPeer var outgoingMessages chan Message // need this to translate from the message IDs the UI uses to the ones the acknowledgement system uses on the wire var acknowledgementIDs map[uint32]uint type Contact struct { Messages []Message Unread int Status connections.ConnectionState } func (this *Contact) AddMessage(m Message) { this.Messages = append(this.Messages, m) } type Message struct { With, Message string FromMe bool MessageID uint Timestamp time.Time } func DeliverMessageToUI(handle, from, displayname, message string, mID uint, fromMe bool, ts time.Time) { _, found := contactMgr[handle] if !found { contactMgr[handle] = &Contact{[]Message{}, 0, 0} } contactMgr[handle].AddMessage(Message{from, message, fromMe, mID, ts}) if gcd.currentOpenConversation == handle { if fromMe { from = "me" } gcd.AppendMessage(from, message, displayname, mID, ts.Format(TIME_FORMAT), randomProfileImage(from)) } else { contactMgr[handle].Unread++ gcd.SetUnread(handle, contactMgr[handle].Unread) } } func init() { GrandCentralDispatcher_QmlRegisterType2("CustomQmlTypes", 1, 0, "GrandCentralDispatcher") } func main() { contactMgr = make(ContactManager) acknowledgementIDs = make(map[uint32]uint) core.QCoreApplication_SetAttribute(core.Qt__AA_EnableHighDpiScaling, true) widgets.NewQApplication(len(os.Args), os.Args) quickcontrols2.QQuickStyle_SetStyle("Universe") view := quick.NewQQuickView(nil) view.SetResizeMode(quick.QQuickView__SizeRootObjectToView) view.SetMinimumHeight(280) view.SetMinimumWidth(300) view.SetTitle("cwtch") gcd = NewGrandCentralDispatcher(nil) view.RootContext().SetContextProperty("gcd", gcd) if len(os.Args) == 2 && os.Args[1] == "local" { view.SetSource(core.QUrl_FromLocalFile("./qml/main.qml")) } else { view.SetSource(core.NewQUrl3("qrc:/qml/main.qml", 0)) } outgoingMessages = make(chan Message, 1000) go postmanPat() initialize(view) view.Show() go torStatusPoller() go presencePoller() go groupPoller() go ricochetListener() widgets.QApplication_Exec() } func groupPoller() { for { time.Sleep(time.Second * 4) servers := peer.GetServers() groups := peer.GetGroups() for i := range groups { group := peer.GetGroup(groups[i]) //log.Printf("setting group %s to status %d", groups[i], int(servers[group.GroupServer])) gcd.SetConnectionStatus(groups[i], int(servers[group.GroupServer])) } } } func presencePoller() { // TODO: make this subscribe-able in ricochet time.Sleep(time.Second * 4) for { contacts := peer.GetContacts() for i := range contacts { _, found := contactMgr[contacts[i]] if !found { // new contact has attempted to connect with us, treat it as an invite contactMgr[contacts[i]] = &Contact{[]Message{}, 0, -1} c, _ := peer.GetProfile().GetContact(contacts[i]) peer.GetProfile().SetCustomAttribute(contacts[i]+"_name", c.Name) peer.GetProfile().SetCustomAttribute(c.Name+"_onion", contacts[i]) peer.Save() gcd.AddContact(c.Name, contacts[i], "", randomProfileImage(contacts[i]), "0", c.Trusted) } c, found := peer.GetPeers()[contacts[i]] if !found && contactMgr[contacts[i]].Status != -2 { //log.Printf("setting %v to -2", contacts[i]) contactMgr[contacts[i]].Status = -2 gcd.SetConnectionStatus(contacts[i], -2) } else if contactMgr[contacts[i]].Status != c { //log.Printf("was: %v", contactMgr[contacts[i]].Status) contactMgr[contacts[i]].Status = c //log.Printf("setting %v to status %v", contacts[i], c) gcd.SetConnectionStatus(contacts[i], int(c)) //log.Printf("now: %v", contactMgr[contacts[i]].Status) } } time.Sleep(time.Second * 4) } } func torStatusPoller() { for { time.Sleep(time.Second) //todo: this should use a config manager //todo: also, try dialing the proxy to differentiate tor not running vs control port not configured rawStatus, err := asaur.GetInfo("localhost:9051", "tcp4", "", "status/bootstrap-phase") if err != nil { gcd.TorStatus(0, "can't find tor. is it running? is the controlport configured?") continue } status := asaur.ParseBootstrapPhase(rawStatus) progress, _ := strconv.Atoi(status["PROGRESS"]) if status["TAG"] == "done" { gcd.TorStatus(3, "tor appears to be running just fine!") continue } if progress == 0 { gcd.TorStatus(1, "tor is trying to start up") continue } gcd.TorStatus(2, status["SUMMARY"]) } } func cwtchListener(groupID string, channel chan model.Message) { for { m := <-channel log.Printf("GROUPMSG %s %s", m.Message, m.PeerID) name := m.PeerID if name == peer.GetProfile().Onion { name = "me" } else { var exists bool // lol this is a golang antifeature name, exists = peer.GetProfile().GetCustomAttribute(m.PeerID + "_name") if !exists { name = "" } } if name == "" { name = m.PeerID[:16] + "..." } DeliverMessageToUI(groupID, m.PeerID, name, m.Message, 0, m.PeerID == peer.GetProfile().Onion, m.Timestamp) peer.Save() } } func ricochetListener() { err := peer.Listen() if err != nil { fmt.Printf("error listening for connections: %v\n", err) gcd.InvokePopup("error handling network connection") } } func postmanPat() { postOffice := make(map[string]chan Message) for { m := <-outgoingMessages _, found := postOffice[m.With] if !found { postOffice[m.With] = make(chan Message, 100) go andHisBlackAndWhiteCat(postOffice[m.With]) } postOffice[m.With] <- m } } func andHisBlackAndWhiteCat(incomingMessages chan Message) { for { m := <-incomingMessages connection := peer.PeerWithOnion(m.With) connection.DoOnChannel("im.ricochet.chat", channels.Outbound, func(channel *channels.Channel) { chatchannel, ok := channel.Handler.(*channels.ChatChannel) if ok { log.Printf("Sending packet") acknowledgementIDs[chatchannel.SendMessage(m.Message)] = m.MessageID } }) } } func initialize(view *quick.QQuickView) { var err error //TODO: this section is ported over and has a lot of printf errors, need to show them in the ui var dirname, filename string if os.Getenv("CWTCH_FOLDER") != "" { dirname = os.Getenv("CWTCH_FOLDER") filename = path.Join(dirname, "keep-this-file-private") } else { usr, err := user.Current() if err != nil { fmt.Printf("\nerror: could not load current user: %v\n", err) os.Exit(1) } dirname = path.Join(usr.HomeDir, ".cwtch") filename = path.Join(dirname, "keep-this-file-private") } /*_, err := app2.NewApp(dirname, "/data/data/org.qtproject.example.go/lib/libtor.so") if err != nil { log.Printf("ERROR CREATING CWTCH APP: %v", err) } time.Sleep(time.Second * 10) */ os.MkdirAll(dirname, 0700) _, err = app.NewApp("/data/data/org.qtproject.example.go/files/", "/data/data/org.qtproject.example.go/lib/libtor.so") log.Printf("starting Swtch app: err: %v\n", err) peer, err = libpeer.LoadCwtchPeer(filename, "be gay do crime") if err != nil { fmt.Println("couldn't load your config file, attempting to create a new one now") names, err := diceware.Generate(1) peer, err = libpeer.NewCwtchPeer(names[0], "be gay do crime", filename) if err != nil { fmt.Println("couldn't create one either :( exiting") os.Exit(1) } peer.Save() } gcd.UpdateMyProfile(peer.GetProfile().Name, peer.GetProfile().Onion, randomProfileImage(peer.GetProfile().Onion)) aif := application.ApplicationInstanceFactory{} aif.Init() app := new(application.RicochetApplication) aif.AddHandler("im.ricochet.chat", func(rai *application.ApplicationInstance) func() channels.Handler { ccl := new(ChatChannelListener) ccl.Init(rai, app) return func() channels.Handler { chat := new(channels.ChatChannel) chat.Handler = ccl return chat } }) peer.SetApplicationInstanceFactory(aif) contacts := peer.GetContacts() for i := range contacts { attr, _ := peer.GetProfile().GetCustomAttribute(contacts[i] + "_name") contactMgr[contacts[i]] = &Contact{[]Message{}, 0, 0} gcd.AddContact(attr, contacts[i], "", randomProfileImage(contacts[i]), "0", peer.GetContact(contacts[i]).Trusted) } groups := peer.GetGroups() for i := range groups { group := peer.GetGroup(groups[i]) group.NewMessage = make(chan model.Message) go cwtchListener(groups[i], group.NewMessage) peer.JoinServer(group.GroupServer) //TODO: base the profileImage off the groupid, not the server. probably by decoding it and then re-encoding it b32 gcd.AddContact(group.GroupID[:12], group.GroupID, group.GroupServer, randomGroupImage(groups[i]), "0", group.Accepted) log.Printf("GROUP %s@%s", group.GroupID, group.GroupServer) } } // temporary until we do real picture selection func randomProfileImage(onion string) string { //TODO: this is a hack, fix ever passing this in if onion == "me" { onion = peer.GetProfile().Onion } choices := []string{"001-centaur", "002-kraken", "003-dinosaur", "004-tree-1", "005-hand", "006-echidna", "007-robot", "008-mushroom", "009-harpy", "010-phoenix", "011-dragon-1", "012-devil", "013-troll", "014-alien", "015-minotaur", "016-madre-monte", "017-satyr", "018-karakasakozou", "019-pirate", "020-werewolf", "021-scarecrow", "022-valkyrie", "023-curupira", "024-loch-ness-monster", "025-tree", "026-cerberus", "027-gryphon", "028-mermaid", "029-vampire", "030-goblin", "031-yeti", "032-leprechaun", "033-medusa", "034-chimera", "035-elf", "036-hydra", "037-cyclops", "038-pegasus", "039-narwhal", "040-woodcutter", "041-zombie", "042-dragon", "043-frankenstein", "044-witch", "045-fairy", "046-genie", "047-pinocchio", "048-ghost", "049-wizard", "050-unicorn"} barr, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion)) if err != nil || len(barr) != 35 { fmt.Printf("error: %v %v %v\n", onion, err, barr) return "qrc:/qml/images/extra/openprivacy.png" } return "qrc:/qml/images/profiles/" + choices[int(barr[33])%len(choices)] + ".png" } func randomGroupImage(onion string) string { choices := []string{"001-borobudur","002-opera-house","003-burj-al-arab","004-chrysler","005-acropolis","006-empire-state-building","007-temple","008-indonesia-1","009-new-zealand","010-notre-dame","011-space-needle","012-seoul","013-mosque","014-milan","015-statue","016-pyramid","017-cologne","018-brandenburg-gate","019-berlin-cathedral","020-hungarian-parliament","021-buckingham","022-thailand","023-independence","024-angkor-wat","025-vaticano","026-christ-the-redeemer","027-colosseum","028-golden-gate-bridge","029-sphinx","030-statue-of-liberty","031-cradle-of-humankind","032-istanbul","033-london-eye","034-sagrada-familia","035-tower-bridge","036-burj-khalifa","037-washington","038-big-ben","039-stonehenge","040-white-house","041-ahu-tongariki","042-capitol","043-eiffel-tower","044-church-of-the-savior-on-spilled-blood","045-arc-de-triomphe","046-windmill","047-louvre","048-torii-gate","049-petronas","050-matsumoto-castle","051-fuji","052-temple-of-heaven","053-pagoda","054-chichen-itza","055-forbidden-city","056-merlion","057-great-wall-of-china","058-taj-mahal","059-pisa","060-indonesia"} barr, err := hex.DecodeString(onion) if err != nil || len(barr) == 0 { fmt.Printf("error: %v %v %v\n", onion, err, barr) return "qrc:/qml/images/extra/openprivacy.png" } return "qrc:/qml/images/servers/" + choices[int(barr[0])%len(choices)] + ".png" }