func (g *gui) newConversation() error { controls, err := g.engine.LoadFile("qrc:///qml/new-conversation.qml") if err != nil { return err } window := controls.CreateWindow(nil) window.ObjectByName("sendMessage").On("triggered", func() { to := window.ObjectByName("toField").String("text") subject := window.ObjectByName("subjectField").String("text") message := window.ObjectByName("messageArea").String("text") participants := make([]string, 0, 2) for _, dst := range strings.Split(to, ",") { participants = append(participants, strings.TrimSpace(dst)) } conv := &proto.ConversationMetadata{ Participants: participants, Subject: subject, } if err := g.ConversationToOutbox(conv); err != nil { log.Printf("failed to create conversation (maybe already sent?): %s", err) } if err := g.MessageToOutbox(persistence.ConversationName(conv), message); err != nil { log.Printf("failed to send message (maybe already sent?): %s", err) } window.Call("closeWindow") //overriding native "close" b/c of weird errors }) return nil }
func (g *gui) handleConversation(con *proto.ConversationMetadata) { if _, already := g.conversationsIndex[persistence.ConversationName(con)]; already { return } err := g.watcher.Add(filepath.Join(g.ConversationDir(), persistence.ConversationName(con))) if err != nil { log.Printf("error watching conversation %s: %s\n", persistence.ConversationName(con), err) // continue after error } qml.Lock() defer qml.Unlock() g.conversationsIndex[persistence.ConversationName(con)] = len(g.conversations) g.conversations = append(g.conversations, con) g.conversationsDisplay.Call("addItem", toJson(con)) }
func (p *Daemon) conversationToConversations(metadata *proto.ConversationMetadata) error { path := filepath.Join(p.ConversationDir(), persistence.ConversationName(metadata)) tmpDir, err := p.MkdirInTemp() if err != nil { return err } defer shred.RemoveAll(tmpDir) if err := p.MarshalToFile(filepath.Join(tmpDir, persistence.MetadataFileName), metadata); err != nil { return err } return os.Rename(filepath.Join(tmpDir), path) }
func (g *gui) openConversation(idx int) error { conv := g.conversations[idx] msgs, err := g.LoadMessages(conv) if err != nil { panic(err) } msgsHTML := "" for _, msg := range msgs { msgsHTML += renderToHTML(msg) + "<br>" } controls, err := g.engine.LoadFile("qrc:///qml/old-conversation.qml") if err != nil { return err } window := controls.CreateWindow(nil) window.ObjectByName("historyArea").Call("append", msgsHTML) window.On("sendMessage", func(message string) { err := g.MessageToOutbox(persistence.ConversationName(conv), message) if err != nil { log.Fatal(err) } }) //TODO: if an open conversation is selected again, focus that window qml.Lock() g.openConversations[persistence.ConversationName(conv)] = window qml.Unlock() window.On("closing", func() { qml.Lock() delete(g.openConversations, persistence.ConversationName(conv)) qml.Unlock() }) return nil }
func (d *Daemon) saveMessage(message *proto.Message) error { // generate metadata file metadata := proto.ConversationMetadata{ Participants: message.Participants, Subject: message.Subject, } // generate conversation name convName := persistence.ConversationName(&metadata) messageName := persistence.MessageName(time.Unix(0, message.Date), string(message.Dename)) // create conversation directory if it doesn't already exist convDir := filepath.Join(d.ConversationDir(), convName) outboxDir := filepath.Join(d.OutboxDir(), convName) _, err := os.Stat(convDir) if err != nil && !os.IsNotExist(err) { return err } else if err != nil && os.IsNotExist(err) { // new message in existing conversation if err := d.conversationToConversations(&metadata); err != nil { return err } } err = d.AtomicWriteFile(filepath.Join(convDir, messageName), message.Contents, 0600) if err != nil { return err } // to outbox tdir, err := d.MkdirInTemp() if err != nil { return err } defer shred.RemoveAll(tdir) err = d.MarshalToFile(filepath.Join(tdir, persistence.MetadataFileName), &metadata) if err != nil { return err } if err = os.Rename(filepath.Join(tdir), outboxDir); err != nil && !os.IsExist(err) && !strings.Contains(err.Error(), "directory not empty") { return err } return nil }
func TestReadFromFiles(t *testing.T) { alice := "alice" bob := "bob" denameConfig, denameTeardown := denameTestutil.SingleServer(t) defer denameTeardown() _, serverPubkey, serverAddr, serverTeardown := server.CreateTestServer(t) defer serverTeardown() aliceDir, err := ioutil.TempDir("", "daemon-alice") if err != nil { t.Fatal(err) } defer shred.RemoveAll(aliceDir) bobDir, err := ioutil.TempDir("", "daemon-bob") if err != nil { t.Fatal(err) } defer shred.RemoveAll(bobDir) aliceDaemon := PrepareTestAccountDaemon(alice, aliceDir, denameConfig, serverAddr, serverPubkey, t) bobDaemon := PrepareTestAccountDaemon(bob, bobDir, denameConfig, serverAddr, serverPubkey, t) //activate initialized daemons aliceDaemon.Start() bobDaemon.Start() defer aliceDaemon.Stop() defer bobDaemon.Stop() //alice creates a new conversation and sends a message participants := []string{alice, bob} subj := "testConversation" conv := &proto.ConversationMetadata{ Participants: participants, Subject: subj, } if err := aliceDaemon.ConversationToOutbox(conv); err != nil { t.Fatal(err) } watcher, err := fsnotify.NewWatcher() if err != nil { t.Fatal(err) } err = watcher.Add(bobDaemon.ConversationDir()) if err != nil { t.Fatal(err) } sentMessage := "Bob, can you hear me?" if err := aliceDaemon.MessageToOutbox(persistence.ConversationName(conv), sentMessage); err != nil { t.Fatal(err) } var files []string for len(files) == 0 { watcher.Add((<-watcher.Events).Name) if files, err = filepath.Glob(filepath.Join(bobDaemon.ConversationDir(), persistence.ConversationName(conv), "*alice")); err != nil { t.Fatal(err) } } receivedMessage, err := ioutil.ReadFile(files[0]) if err != nil { t.Fatal(err) } if string(receivedMessage) != sentMessage { t.Fatal("receivedMessage != sentMessage") } }
func (d *Daemon) processOutboxDir(dirname string) error { // TODO: refactor: separate message assembly and filesystem access? // parse metadata metadataFile := filepath.Join(dirname, persistence.MetadataFileName) if _, err := os.Stat(metadataFile); err != nil { return nil // no metadata --> not an outgoing message } metadata := proto.ConversationMetadata{} err := persistence.UnmarshalFromFile(metadataFile, &metadata) if err != nil { return err } metadata.Participants = append(metadata.Participants, d.Dename) undupStrings(metadata.Participants) sort.Strings(metadata.Participants) convName := persistence.ConversationName(&metadata) // load messages potentialMessages, err := ioutil.ReadDir(dirname) if err != nil { return err } messages := make([][]byte, 0, len(potentialMessages)) for _, finfo := range potentialMessages { if !finfo.IsDir() && finfo.Name() != persistence.MetadataFileName { msg, err := ioutil.ReadFile(filepath.Join(dirname, finfo.Name())) if err != nil { return err } // make protobuf for message; append it d.ourDenameLookupMu.Lock() payload := proto.Message{ Dename: d.Dename, DenameLookup: d.ourDenameLookup, Contents: msg, Subject: metadata.Subject, Participants: metadata.Participants, Date: finfo.ModTime().UnixNano(), } d.ourDenameLookupMu.Unlock() payloadBytes, err := payload.Marshal() if err != nil { return err } messages = append(messages, payloadBytes) } } if len(messages) == 0 { return nil // no messages to send, just the metadata file } if err := d.conversationToConversations(&metadata); err != nil && !os.IsExist(err) && !strings.Contains(fmt.Sprint(err), "directory not empty") { log.Fatal(err) } for _, recipient := range metadata.Participants { if recipient == d.Dename { continue } for _, msg := range messages { if err != nil { return err } if msgRatch, err := LoadRatchet(d, recipient, d.fillAuth, d.checkAuth); err != nil { //First message to this recipien if err := d.sendFirstMessage(msg, recipient); err != nil { return err } } else { if err := d.sendMessage(msg, recipient, msgRatch); err != nil { return err } } } } // move the sent messages to the conversation folder for _, finfo := range potentialMessages { if !finfo.IsDir() && finfo.Name() != persistence.MetadataFileName { if err = os.Rename(filepath.Join(dirname, finfo.Name()), filepath.Join(d.ConversationDir(), persistence.ConversationName(&metadata), persistence.MessageName(finfo.ModTime(), string(d.Dename)))); err != nil { log.Fatal(err) } } } // canonicalize the outbox folder name if dirname != filepath.Join(d.OutboxDir(), convName) { if err := os.Rename(dirname, filepath.Join(d.OutboxDir(), convName)); err != nil { shred.RemoveAll(dirname) } } return nil }