Пример #1
0
func (c *cliClient) processCommand(cmd interface{}) (shouldQuit bool) {
	// First commands that might start a subprocess that needs terminal
	// control.
	switch cmd.(type) {
	case composeCommand:
		if contact, ok := c.currentObj.(*Contact); ok {
			c.compose(contact, nil, nil)
		} else {
			c.Printf("%s Select contact first\n", termWarnPrefix)
		}

	case contactsCommand:
		c.showContacts()
	case editCommand:
		if draft, ok := c.currentObj.(*Draft); ok {
			c.compose(nil, draft, nil)
		} else {
			c.Printf("%s Select draft first\n", termWarnPrefix)
		}

	case replyCommand:
		msg, ok := c.currentObj.(*InboxMessage)
		if !ok {
			c.Printf("%s Select inbox message first\n", termWarnPrefix)
			return
		}
		c.compose(c.contacts[msg.from], nil, msg)

	default:
		goto Handle
	}
	return

Handle:
	// The command won't need to start subprocesses with terminal control
	// so we can start watching for Ctrl-C again.
	c.termWrapper.Restart()

	switch cmd := cmd.(type) {
	case clearCommand:
		c.Printf("\x1b[2J")

	case helpCommand:
		if cmd.ShowAll {
			c.input.showHelp(0, true)
			return
		}

		switch c.currentObj.(type) {
		case *Contact:
			c.input.showHelp(contextContact, false)
		case *Draft:
			c.input.showHelp(contextDraft, false)
		case *InboxMessage:
			c.input.showHelp(contextInbox, false)
		case *queuedMessage:
			c.input.showHelp(contextOutbox, false)
		default:
			c.input.showHelp(0, false)
		}

	case tagCommand:
		if len(cmd.tag) == 0 {
			c.showState()
			return
		}
		cliId, ok := cliIdFromString(cmd.tag)
		if !ok {
			c.Printf("%s Bad tag\n", termWarnPrefix)
			return
		}
		for _, msg := range c.inbox {
			if msg.cliId == cliId {
				c.setCurrentObject(msg)
				return
			}
		}
		for _, msg := range c.outbox {
			if msg.cliId == cliId {
				c.setCurrentObject(msg)
				return
			}
		}
		for _, msg := range c.drafts {
			if msg.cliId == cliId {
				c.setCurrentObject(msg)
				return
			}
		}
		for _, contact := range c.contacts {
			if contact.cliId == cliId {
				c.setCurrentObject(contact)
				return
			}
		}
		c.Printf("%s Unknown tag\n", termWarnPrefix)

	case logCommand:
		n := 15
		if l := len(c.log.entries); l < n {
			n = l
		}
		for _, entry := range c.log.entries[len(c.log.entries)-n:] {
			c.Printf("%s (%s) %s\n", termHeaderPrefix, entry.Format(logTimeFormat), terminalEscape(entry.s, false))
		}

	case transactNowCommand:
		c.Printf("%s Triggering immediate network transaction.\n", termPrefix)
		select {
		case c.fetchNowChan <- nil:
		default:
		}

	case closeCommand:
		c.setCurrentObject(nil)

	case quitCommand:
		c.ShutdownAndSuspend()
		c.Printf("Goodbye!\n")
		shouldQuit = true
		return

	case deleteCommand:
		if c.currentObj == nil {
			c.Printf("%s Select object first\n", termWarnPrefix)
			return
		}
		switch o := c.currentObj.(type) {
		case *Draft:
			delete(c.drafts, o.id)
			c.save()
			c.setCurrentObject(nil)
		case *Contact:
			c.maybeDeleteContact(o)
			// maybeDeleteContact may need confirmation so
			// setCurrentObject is handled in there.
		default:
			c.Printf("%s Cannot delete current object\n", termWarnPrefix)
		}

	case sendCommand:
		draft, ok := c.currentObj.(*Draft)
		if !ok {
			c.Printf("%s Select draft first\n", termWarnPrefix)
			return
		}
		to := c.contacts[draft.to]
		var myNextDH []byte
		if to.ratchet == nil {
			var nextDHPub [32]byte
			curve25519.ScalarBaseMult(&nextDHPub, &to.currentDHPrivate)
			myNextDH = nextDHPub[:]
		}

		if len(draft.body) == 0 {
			// Zero length bodies are ACKs.
			draft.body = " "
		}
		id := c.randId()
		var inReplyTo *uint64
		if r := draft.inReplyTo; r != 0 {
			inReplyTo = proto.Uint64(r)
		}
		err := c.send(to, &pond.Message{
			Id:               proto.Uint64(id),
			Time:             proto.Int64(c.Now().Unix()),
			Body:             []byte(draft.body),
			BodyEncoding:     pond.Message_RAW.Enum(),
			InReplyTo:        inReplyTo,
			MyNextDh:         myNextDH,
			Files:            draft.attachments,
			DetachedFiles:    draft.detachments,
			SupportedVersion: proto.Int32(protoVersion),
		})
		if err != nil {
			c.log.Errorf("%s Error sending: %s\n", termErrPrefix, err)
			return
		}
		if draft.inReplyTo != 0 {
			for _, msg := range c.inbox {
				if msg.message != nil && msg.message.GetId() == draft.inReplyTo {
					msg.acked = true
					break
				}
			}
		}
		delete(c.drafts, draft.id)
		c.setCurrentObject(nil)
		for _, msg := range c.outbox {
			if msg.id == id {
				if msg.cliId == invalidCliId {
					msg.cliId = c.newCliId()
				}
				c.Printf("%s Created new outbox entry %s%s%s\n", termInfoPrefix, termCliIdStart, msg.cliId.String(), termReset)
				c.setCurrentObject(msg)
				c.showQueueState()
				break
			}
		}
		c.save()

	case abortCommand:
		msg, ok := c.currentObj.(*queuedMessage)
		if !ok {
			c.Printf("%s Select outbox message first\n", termErrPrefix)
			return
		}

		c.queueMutex.Lock()
		index := c.indexOfQueuedMessage(msg)
		if index == -1 || msg.sending {
			c.queueMutex.Unlock()
			c.Printf("%s Too Late to Abort!\n", termErrPrefix)
			return
		}

		c.removeQueuedMessage(index)
		c.queueMutex.Unlock()

		c.deleteOutboxMsg(msg.id)
		draft := c.outboxToDraft(msg)
		c.drafts[draft.id] = draft
		if draft.cliId == invalidCliId {
			draft.cliId = c.newCliId()
		}

		c.Printf("%s Aborted sending %s%s%s and moved to Drafts as %s%s%s\n", termInfoPrefix, termCliIdStart, msg.cliId.String(), termReset, termCliIdStart, draft.cliId.String(), termReset)
		c.save()
		c.setCurrentObject(draft)

	case ackCommand:
		msg, ok := c.currentObj.(*InboxMessage)
		if !ok {
			c.Printf("%s Select inbox message first\n", termWarnPrefix)
			return
		}
		if msg.acked {
			c.Printf("%s Message has already been acknowledged\n", termWarnPrefix)
			return
		}
		msg.acked = true
		c.sendAck(msg)
		c.showQueueState()

	case showCommand:
		if c.currentObj == nil {
			c.Printf("Select object first\n")
			return
		}
		switch o := c.currentObj.(type) {
		case *queuedMessage:
			c.showOutbox(o)
		case *InboxMessage:
			c.showInbox(o)
		case *Draft:
			c.showDraft(o)
		case *Contact:
			c.showContact(o)
		default:
			c.Printf("%s Cannot show the current object\n", termWarnPrefix)
		}

	case showOutboxSummaryCommand:
		c.showOutboxSummary()

	case showInboxSummaryCommand:
		c.showInboxSummary()

	case showDraftsSummaryCommand:
		c.showDraftsSummary()

	case showQueueStateCommand:
		c.showQueueState()

	case statusCommand:
		c.showState()

	case attachCommand:
		draft, ok := c.currentObj.(*Draft)
		if !ok {
			c.Printf("%s Select draft first\n", termWarnPrefix)
		}
		contents, size, err := openAttachment(cmd.Filename)
		if err != nil {
			c.Printf("%s Failed to open file: %s\n", termErrPrefix, terminalEscape(err.Error(), false))
			return
		}
		if size > 0 {
			c.Printf("%s File is too large (%d bytes) to attach. Use the 'upload' or 'save-encrypted' commands to encrypt the file, include just the keys in the message and either upload or save the ciphertext\n", termErrPrefix, size)
			return
		}

		base := filepath.Base(cmd.Filename)
		a := &pond.Message_Attachment{
			Filename: proto.String(base),
			Contents: contents,
		}
		draft.attachments = append(draft.attachments, a)
		c.Printf("%s Attached '%s' (%d bytes)\n", termPrefix, terminalEscape(base, false), len(contents))
		c.printDraftSize(draft)

	case uploadCommand:
		draft, ok := c.currentObj.(*Draft)
		if !ok {
			c.Printf("%s Select draft first\n", termWarnPrefix)
			return
		}

		base := filepath.Base(cmd.Filename)
		id := c.randId()
		c.Printf("%s Padding, encrypting and uploading '%s' to home server (Ctrl-C to abort):\n", termPrefix, terminalEscape(base, false))
		cancelThunk := c.startUpload(id, cmd.Filename)

		if detachment, ok := c.runBackgroundProcess(id, cancelThunk); ok {
			draft.detachments = append(draft.detachments, detachment)
		}

	case downloadCommand:
		msg, ok := c.currentObj.(*InboxMessage)
		if !ok {
			c.Printf("%s Select inbox message\n", termWarnPrefix)
			return
		}
		i, ok := c.prepareSubobjectCommand(cmd.Number, len(msg.message.DetachedFiles), "detachment")
		if !ok {
			return
		}
		id := c.randId()

		c.Printf("%s Downloading and decrypting detachment (Ctrl-C to abort):\n", termPrefix)
		cancelThunk := c.startDownload(id, cmd.Filename, msg.message.DetachedFiles[i])

		c.runBackgroundProcess(id, cancelThunk)

	case saveCommand:
		msg, ok := c.currentObj.(*InboxMessage)
		if !ok {
			c.Printf("%s Select inbox message\n", termWarnPrefix)
			return
		}
		i, ok := c.prepareSubobjectCommand(cmd.Number, len(msg.message.Files), "attachment")
		if !ok {
			return
		}

		if err := ioutil.WriteFile(cmd.Filename, msg.message.Files[i].GetContents(), 0600); err != nil {
			c.Printf("%s Failed to write file: %s\n", termErrPrefix, terminalEscape(err.Error(), false))
		} else {
			c.Printf("%s Wrote file\n", termPrefix)
		}

	case removeCommand:
		draft, ok := c.currentObj.(*Draft)
		if !ok {
			c.Printf("%s Select draft first\n", termWarnPrefix)
		}
		i, ok := c.prepareSubobjectCommand(cmd.Number, len(draft.attachments)+len(draft.detachments), "attachment")
		if !ok {
			return
		}

		if i < len(draft.attachments) {
			draft.attachments = append(draft.attachments[:i], draft.attachments[i+1:]...)
			return
		}
		i -= len(draft.attachments)
		draft.detachments = append(draft.detachments[:i], draft.detachments[i+1:]...)

	case newContactCommand:
		for _, contact := range c.contacts {
			if contact.name == cmd.Name {
				c.Printf("%s A contact with that name already exists.\n", termErrPrefix)
				return
			}
		}

		c.Printf("Enter shared secret with contact, or hit enter to generate, print and use a random one\n")
		sharedSecret, err := c.term.ReadPassword("secret: ")
		if err != nil {
			panic(err)
		}
		if len(sharedSecret) == 0 {
			var secret [16]byte
			c.randBytes(secret[:])
			sharedSecret = fmt.Sprintf("%x", secret[:])
			c.Printf("%s Shared secret: %s\n", termPrefix, sharedSecret)
		}

		contact := &Contact{
			name:      cmd.Name,
			isPending: true,
			id:        c.randId(),
			cliId:     c.newCliId(),
		}

		c.newKeyExchange(contact)

		stack := &panda.CardStack{
			NumDecks: 1,
		}
		secret := panda.SharedSecret{
			Secret: sharedSecret,
			Cards:  *stack,
		}

		mp := c.newMeetingPlace()

		c.contacts[contact.id] = contact
		kx, err := panda.NewKeyExchange(c.rand, mp, &secret, contact.kxsBytes)
		if err != nil {
			panic(err)
		}
		kx.Testing = c.testing
		contact.pandaKeyExchange = kx.Marshal()
		contact.kxsBytes = nil

		c.save()
		c.pandaWaitGroup.Add(1)
		contact.pandaShutdownChan = make(chan struct{})
		go c.runPANDA(contact.pandaKeyExchange, contact.id, contact.name, contact.pandaShutdownChan)
		c.Printf("%s Key exchange running in background.\n", termPrefix)

	case renameCommand:
		if contact, ok := c.currentObj.(*Contact); ok {
			c.renameContact(contact, cmd.NewName)
		} else {
			c.Printf("%s Select contact first\n", termWarnPrefix)
		}

	default:
		panic(fmt.Sprintf("Unhandled command: %#v", cmd))
	}

	return
}
Пример #2
0
func (c *cliClient) processCommand(cmd interface{}) (shouldQuit bool) {
	// First commands that might start a subprocess that needs terminal
	// control.
	switch cmd.(type) {
	case composeCommand:
		if contact, ok := c.currentObj.(*Contact); ok {
			c.compose(contact, nil, nil)
		} else {
			c.Printf("%s Select contact first\n", termWarnPrefix)
		}

	case editCommand:
		if draft, ok := c.currentObj.(*Draft); ok {
			if draft.to == 0 {
				c.Printf("%s Draft was created in the GUI and doesn't have a destination specified. Please use the GUI to manipulate this draft.\n", termErrPrefix)
				return
			}
			c.compose(nil, draft, nil)
		} else {
			c.Printf("%s Select draft first\n", termWarnPrefix)
		}

	case replyCommand:
		msg, ok := c.currentObj.(*InboxMessage)
		if !ok {
			c.Printf("%s Select inbox message first\n", termWarnPrefix)
			return
		}
		if msg.from == 0 {
			c.Printf("%s Cannot reply to server announcement\n", termWarnPrefix)
			return
		}
		c.compose(c.contacts[msg.from], nil, msg)

	default:
		goto Handle
	}
	return

Handle:
	// The command won't need to start subprocesses with terminal control
	// so we can start watching for Ctrl-C again.
	c.termWrapper.Restart()

	switch cmd := cmd.(type) {
	case clearCommand:
		c.Printf("\x1b[2J")

	case helpCommand:
		if cmd.ShowAll {
			c.input.showHelp(0, true)
			return
		}

		switch c.currentObj.(type) {
		case *Contact:
			c.input.showHelp(contextContact, false)
		case *Draft:
			c.input.showHelp(contextDraft, false)
		case *InboxMessage:
			c.input.showHelp(contextInbox, false)
		case *queuedMessage:
			c.input.showHelp(contextOutbox, false)
		default:
			c.input.showHelp(0, false)
		}

	case tagCommand:
		if len(cmd.tag) == 0 {
			c.showState()
			return
		}
		cliId, ok := cliIdFromString(cmd.tag)
		if !ok {
			c.Printf("%s Bad tag\n", termWarnPrefix)
			return
		}
		for _, msg := range c.inbox {
			if msg.cliId == cliId {
				c.setCurrentObject(msg)
				return
			}
		}
		for _, msg := range c.outbox {
			if msg.cliId == cliId {
				c.setCurrentObject(msg)
				return
			}
		}
		for _, msg := range c.drafts {
			if msg.cliId == cliId {
				c.setCurrentObject(msg)
				return
			}
		}
		for _, contact := range c.contacts {
			if contact.cliId == cliId {
				c.setCurrentObject(contact)
				return
			}
		}
		c.Printf("%s Unknown tag\n", termWarnPrefix)

	case logCommand:
		n := 15
		if l := len(c.log.entries); l < n {
			n = l
		}
		table := cliTable{
			rows:         make([]cliRow, 0, n),
			noIndicators: true,
		}

		for _, entry := range c.log.entries[len(c.log.entries)-n:] {
			table.rows = append(table.rows, cliRow{
				cols: []string{
					entry.Format(logTimeFormat),
					terminalEscape(entry.s, false),
				},
			})
		}

		table.WriteTo(c.term)

	case transactNowCommand:
		c.Printf("%s Triggering immediate network transaction.\n", termPrefix)
		select {
		case c.fetchNowChan <- nil:
		default:
		}

	case closeCommand:
		c.setCurrentObject(nil)

	case quitCommand:
		c.ShutdownAndSuspend()
		c.Printf("Goodbye!\n")
		shouldQuit = true
		return

	case deleteCommand:
		if c.currentObj == nil {
			c.Printf("%s Select object first\n", termWarnPrefix)
			return
		}
		if !c.deleteArmed {
			switch obj := c.currentObj.(type) {
			case *Contact:
				c.Printf("%s You attempted to delete a contact (%s). Doing so removes all messages to and from that contact and revokes their ability to send you messages. To confirm, enter the delete command again.\n", termWarnPrefix, terminalEscape(obj.name, false))
			case *Draft:
				toName := "<unknown>"
				if obj.to != 0 {
					toName = c.ContactName(obj.to)
				}
				c.Printf("%s You attempted to delete a draft message (to %s). To confirm, enter the delete command again.\n", termWarnPrefix, terminalEscape(toName, false))
			case *queuedMessage:
				c.queueMutex.Lock()
				if c.indexOfQueuedMessage(obj) != -1 {
					c.queueMutex.Unlock()
					c.Printf("%s Please abort the unsent message before deleting it.\n", termErrPrefix)
					return
				}
				c.queueMutex.Unlock()
				c.Printf("%s You attempted to delete a message (to %s). To confirm, enter the delete command again.\n", termWarnPrefix, terminalEscape(c.ContactName(obj.to), false))
			case *InboxMessage:
				c.Printf("%s You attempted to delete a message (from %s). To confirm, enter the delete command again.\n", termWarnPrefix, terminalEscape(c.ContactName(obj.from), false))
			default:
				c.Printf("%s Cannot delete current object\n", termWarnPrefix)
				return
			}
			c.deleteArmed = true
			return
		}
		c.deleteArmed = false

		switch obj := c.currentObj.(type) {
		case *Contact:
			c.deleteContact(obj)
		case *Draft:
			delete(c.drafts, obj.id)
		case *queuedMessage:
			c.deleteOutboxMsg(obj.id)
		case *InboxMessage:
			c.deleteInboxMsg(obj.id)
		default:
			c.Printf("%s Cannot delete current object\n", termWarnPrefix)
			return
		}
		c.setCurrentObject(nil)
		c.save()

	case sendCommand:
		draft, ok := c.currentObj.(*Draft)
		if !ok {
			c.Printf("%s Select draft first\n", termWarnPrefix)
			return
		}
		if draft.to == 0 {
			c.Printf("%s Draft was created in the GUI and doesn't have a destination specified. Please use the GUI to manipulate this draft.\n", termErrPrefix)
			return
		}
		id, _, err := c.sendDraft(draft)
		if err != nil {
			c.Printf("%s Error sending: %s\n", termErrPrefix, err)
			return
		}
		if draft.inReplyTo != 0 {
			for _, msg := range c.inbox {
				if msg.message != nil && msg.message.GetId() == draft.inReplyTo {
					msg.acked = true
					break
				}
			}
		}
		delete(c.drafts, draft.id)
		c.setCurrentObject(nil)
		for _, msg := range c.outbox {
			if msg.id == id {
				if msg.cliId == invalidCliId {
					msg.cliId = c.newCliId()
				}
				c.Printf("%s Created new outbox entry %s%s%s\n", termInfoPrefix, termCliIdStart, msg.cliId.String(), termReset)
				c.setCurrentObject(msg)
				c.showQueueState()
				break
			}
		}
		c.save()

	case abortCommand:
		msg, ok := c.currentObj.(*queuedMessage)
		if !ok {
			c.Printf("%s Select outbox message first\n", termErrPrefix)
			return
		}

		c.queueMutex.Lock()
		index := c.indexOfQueuedMessage(msg)
		if index == -1 || msg.sending {
			c.queueMutex.Unlock()
			c.Printf("%s Too Late to Abort!\n", termErrPrefix)
			return
		}

		c.removeQueuedMessage(index)
		c.queueMutex.Unlock()

		c.deleteOutboxMsg(msg.id)
		draft := c.outboxToDraft(msg)
		c.drafts[draft.id] = draft
		if draft.cliId == invalidCliId {
			draft.cliId = c.newCliId()
		}

		c.Printf("%s Aborted sending %s%s%s and moved to Drafts as %s%s%s\n", termInfoPrefix, termCliIdStart, msg.cliId.String(), termReset, termCliIdStart, draft.cliId.String(), termReset)
		c.save()
		c.setCurrentObject(draft)

	case ackCommand:
		msg, ok := c.currentObj.(*InboxMessage)
		if !ok {
			c.Printf("%s Select inbox message first\n", termWarnPrefix)
			return
		}
		if msg.acked {
			c.Printf("%s Message has already been acknowledged\n", termWarnPrefix)
			return
		}
		if msg.from == 0 {
			c.Printf("%s Cannot ack server announcement\n", termWarnPrefix)
			return
		}
		msg.acked = true
		c.sendAck(msg)
		c.showQueueState()

	case showCommand:
		if c.currentObj == nil {
			c.Printf("Select object first\n")
			return
		}
		switch o := c.currentObj.(type) {
		case *queuedMessage:
			c.showOutbox(o)
		case *InboxMessage:
			c.showInbox(o)
		case *Draft:
			c.showDraft(o)
		case *Contact:
			c.showContact(o)
		default:
			c.Printf("%s Cannot show the current object\n", termWarnPrefix)
		}

	case showIdentityCommand:
		c.showIdentity()

	case showInboxSummaryCommand:
		c.inboxSummary().WriteTo(c.term)

	case showOutboxSummaryCommand:
		c.outboxSummary().WriteTo(c.term)

	case showDraftsSummaryCommand:
		c.draftsSummary().WriteTo(c.term)

	case showContactsCommand:
		c.contactsSummary().WriteTo(c.term)

	case showQueueStateCommand:
		c.showQueueState()

	case statusCommand:
		c.showState()

	case attachCommand:
		draft, ok := c.currentObj.(*Draft)
		if !ok {
			c.Printf("%s Select draft first\n", termWarnPrefix)
			return
		}
		contents, size, err := openAttachment(cmd.Filename)
		if err != nil {
			c.Printf("%s Failed to open file: %s\n", termErrPrefix, terminalEscape(err.Error(), false))
			return
		}
		if size > 0 {
			c.Printf("%s File is too large (%d bytes) to attach. Use the 'upload' command to encrypt the file and upload it to your home server. Pond will include the key in the current draft.\n", termErrPrefix, size)
			return
		}

		base := filepath.Base(cmd.Filename)
		a := &pond.Message_Attachment{
			Filename: proto.String(base),
			Contents: contents,
		}
		draft.attachments = append(draft.attachments, a)
		c.Printf("%s Attached '%s' (%d bytes)\n", termPrefix, terminalEscape(base, false), len(contents))
		c.printDraftSize(draft)

	case uploadCommand:
		draft, ok := c.currentObj.(*Draft)
		if !ok {
			c.Printf("%s Select draft first\n", termWarnPrefix)
			return
		}

		base := filepath.Base(cmd.Filename)
		id := c.randId()
		c.Printf("%s Padding, encrypting and uploading '%s' to home server (Ctrl-C to abort):\n", termPrefix, terminalEscape(base, false))
		cancelThunk := c.startUpload(id, cmd.Filename)

		if detachment, ok := c.runBackgroundProcess(id, cancelThunk); ok {
			draft.detachments = append(draft.detachments, detachment)
		}

	case downloadCommand:
		msg, ok := c.currentObj.(*InboxMessage)
		if !ok {
			c.Printf("%s Select inbox message\n", termWarnPrefix)
			return
		}
		i, ok := c.prepareSubobjectCommand(cmd.Number, len(msg.message.DetachedFiles), "detachment")
		if !ok {
			return
		}
		id := c.randId()

		if msg.message.DetachedFiles[i].Url == nil {
			c.Printf("%s That detachment is just a key; you need to obtain the encrypted payload out-of-band. Use the save-key command and the decrypt utility the decrypt the payload.\n", termErrPrefix)
			return
		}

		c.Printf("%s Downloading and decrypting detachment (Ctrl-C to abort):\n", termPrefix)
		cancelThunk := c.startDownload(id, cmd.Filename, msg.message.DetachedFiles[i])

		c.runBackgroundProcess(id, cancelThunk)

	case saveKeyCommand:
		msg, ok := c.currentObj.(*InboxMessage)
		if !ok {
			c.Printf("%s Select inbox message\n", termWarnPrefix)
			return
		}
		i, ok := c.prepareSubobjectCommand(cmd.Number, len(msg.message.DetachedFiles), "detachment")
		if !ok {
			return
		}

		if msg.message.DetachedFiles[i].Url != nil {
			c.Printf("%s (Note that this detachment can be downloaded with the 'download' command)\n", termInfoPrefix)
		}

		bytes, err := proto.Marshal(msg.message.DetachedFiles[i])
		if err != nil {
			panic(err)
		}

		if err := ioutil.WriteFile(cmd.Filename, bytes, 0600); err != nil {
			c.Printf("%s Failed to write file: %s\n", termErrPrefix, terminalEscape(err.Error(), false))
		} else {
			c.Printf("%s Wrote file\n", termPrefix)
		}

	case saveCommand:
		msg, ok := c.currentObj.(*InboxMessage)
		if !ok {
			c.Printf("%s Select inbox message\n", termWarnPrefix)
			return
		}
		i, ok := c.prepareSubobjectCommand(cmd.Number, len(msg.message.Files), "attachment")
		if !ok {
			return
		}

		if err := ioutil.WriteFile(cmd.Filename, msg.message.Files[i].GetContents(), 0600); err != nil {
			c.Printf("%s Failed to write file: %s\n", termErrPrefix, terminalEscape(err.Error(), false))
		} else {
			c.Printf("%s Wrote file\n", termPrefix)
		}

	case removeCommand:
		draft, ok := c.currentObj.(*Draft)
		if !ok {
			c.Printf("%s Select draft first\n", termWarnPrefix)
			return
		}
		i, ok := c.prepareSubobjectCommand(cmd.Number, len(draft.attachments)+len(draft.detachments), "attachment")
		if !ok {
			return
		}

		if i < len(draft.attachments) {
			draft.attachments = append(draft.attachments[:i], draft.attachments[i+1:]...)
			return
		}
		i -= len(draft.attachments)
		draft.detachments = append(draft.detachments[:i], draft.detachments[i+1:]...)

	case newContactCommand:
		for _, contact := range c.contacts {
			if contact.name == cmd.Name {
				c.Printf("%s A contact with that name already exists.\n", termErrPrefix)
				return
			}
		}

		var sharedSecret string

		for {
			c.Printf("Enter shared secret with contact, or hit enter to generate, print and use a random one\n")
			var err error
			sharedSecret, err = c.term.ReadPassword("secret: ")
			if err != nil {
				panic(err)
			}
			if len(sharedSecret) == 0 || panda.IsAcceptableSecretString(sharedSecret) {
				break
			}
			c.Printf("%s Checksum incorrect. Please try again.\n", termErrPrefix)
		}

		if len(sharedSecret) == 0 {
			sharedSecret = panda.NewSecretString(c.rand)
			c.Printf("%s Shared secret: %s\n", termPrefix, sharedSecret)
		}

		contact := &Contact{
			name:      cmd.Name,
			isPending: true,
			id:        c.randId(),
			cliId:     c.newCliId(),
		}

		c.newKeyExchange(contact)

		stack := &panda.CardStack{
			NumDecks: 1,
		}
		secret := panda.SharedSecret{
			Secret: sharedSecret,
			Cards:  *stack,
		}

		mp := c.newMeetingPlace()

		c.contacts[contact.id] = contact
		kx, err := panda.NewKeyExchange(c.rand, mp, &secret, contact.kxsBytes)
		if err != nil {
			panic(err)
		}
		kx.Testing = c.testing
		contact.pandaKeyExchange = kx.Marshal()
		contact.kxsBytes = nil

		c.save()
		c.pandaWaitGroup.Add(1)
		contact.pandaShutdownChan = make(chan struct{})
		go c.runPANDA(contact.pandaKeyExchange, contact.id, contact.name, contact.pandaShutdownChan)
		c.Printf("%s Key exchange running in background.\n", termPrefix)

	case renameCommand:
		if contact, ok := c.currentObj.(*Contact); ok {
			c.renameContact(contact, cmd.NewName)
		} else {
			c.Printf("%s Select contact first\n", termWarnPrefix)
		}

	case retainCommand:
		msg, ok := c.currentObj.(*InboxMessage)
		if !ok {
			c.Printf("%s Select inbox message first\n", termWarnPrefix)
			return
		}
		msg.retained = true
		c.save()

	case dontRetainCommand:
		msg, ok := c.currentObj.(*InboxMessage)
		if !ok {
			c.Printf("%s Select inbox message first\n", termWarnPrefix)
			return
		}
		msg.retained = false
		msg.exposureTime = c.Now()
		// TODO: the CLI needs to expire messages when open as the GUI
		// does. See guiClient.processTimer.
		c.save()

	default:
		panic(fmt.Sprintf("Unhandled command: %#v", cmd))
	}

	return
}
Пример #3
0
Файл: ui.go Проект: nico/pond
func (c *client) newContactPanda(contact *Contact, existing bool, nextRow int) interface{} {
	c.newKeyExchange(contact)
	c.contacts[contact.id] = contact

	controls := Grid{
		widgetBase: widgetBase{name: "controls", margin: 5},
		rowSpacing: 5,
		colSpacing: 5,
		rows: [][]GridE{
			{
				{1, 1, Label{
					widgetBase: widgetBase{font: fontMainLabel, foreground: colorHeaderForeground, hAlign: AlignEnd, vAlign: AlignCenter},
					text:       "Shared secret",
				}},
				{2, 1, Entry{widgetBase: widgetBase{name: "shared"}}},
			},
			{
				{1, 1, Label{
					widgetBase: widgetBase{font: fontMainLabel, foreground: colorHeaderForeground, hAlign: AlignEnd, vAlign: AlignCenter},
					text:       "Cards",
				}},
				{1, 1, Entry{widgetBase: widgetBase{name: "cardentry"}, updateOnChange: true}},
				{1, 1, RadioGroup{widgetBase: widgetBase{name: "numdecks"}, labels: []string{"1 deck", "2 decks"}}},
			},
			{
				{1, 1, nil},
				{2, 1, Grid{
					widgetBase: widgetBase{name: "cards"},
					rowSpacing: 2,
					colSpacing: 2,
				}},
			},
			{
				{1, 1, Label{
					widgetBase: widgetBase{font: fontMainLabel, foreground: colorHeaderForeground, hAlign: AlignEnd, vAlign: AlignCenter},
					text:       "When",
				}},
				{2, 1, Grid{
					rowSpacing: 5,
					colSpacing: 3,
					rows: [][]GridE{
						{
							{1, 1, CheckButton{widgetBase: widgetBase{name: "hastime"}, text: "Include time"}},
						},
						{
							{1, 1, Calendar{widgetBase: widgetBase{name: "cal", insensitive: true}}},
							{1, 1, Grid{
								widgetBase: widgetBase{marginLeft: 5},
								rowSpacing: 5,
								colSpacing: 3,
								rows: [][]GridE{
									{
										{1, 1, Label{widgetBase: widgetBase{vAlign: AlignCenter}, text: "Hour"}},
										{1, 1, SpinButton{widgetBase: widgetBase{name: "hour", insensitive: true}, min: 0, max: 23, step: 1}},
									},
									{
										{1, 1, Label{widgetBase: widgetBase{vAlign: AlignCenter}, text: "Minute"}},
										{1, 1, SpinButton{widgetBase: widgetBase{name: "minute", insensitive: true}, min: 0, max: 59, step: 1}},
									},
								},
							}},
						},
					},
				}},
			},
			{
				{1, 1, Button{widgetBase: widgetBase{name: "begin"}, text: "Begin"}},
			},
		},
	}

	rows := [][]GridE{
		{
			{1, 1, Label{text: "3."}},
			{1, 1, Label{text: "Enter the shared secret."}},
		},
		{
			{1, 1, nil},
			{1, 1, Label{text: `The shared secret can be a phrase, or can be generated by shuffling one or two decks of cards together, splitting the stack roughly in half and giving one half to each person. (Or you can do both the card trick and have a phrase.) Additionally, it's possible to use the time of the meeting as a salt if you agreed on it.

When entering the cards enter the number or face of the card first, and then the suite - both as single letters. So the three of dimonds is '3d' and the ace of spades is 'as'. Discard the jokers. Click on a card to delete.`, wrap: 400}},
		},
		{
			{1, 1, nil},
			{1, 1, controls},
		},
	}

	for _, row := range rows {
		c.ui.Actions() <- InsertRow{name: "grid", pos: nextRow, row: row}
		nextRow++
	}
	c.ui.Actions() <- UIState{uiStateNewContact2}
	c.ui.Signal()

	const cardsPerRow = 10
	type gridPoint struct {
		col, row int
	}
	// freeList contains the `holes' in the card grid due to cards being
	// deleted.
	var freeList []gridPoint
	// nextPoint contains the next location to insert into the grid of cards.
	var nextPoint gridPoint
	stack := &panda.CardStack{
		NumDecks: 1,
	}
	cardAtLocation := make(map[gridPoint]panda.Card)
	minDecks := 1
	timeEnabled := false

SharedSecretEvent:
	for {
		event, wanted := c.nextEvent()
		if wanted {
			return event
		}

		if update, ok := event.(Update); ok && update.name == "cardentry" && len(update.text) >= 2 {
			cardText := update.text[:2]
			if cardText == "10" {
				if len(update.text) >= 3 {
					cardText = update.text[:3]
				} else {
					continue SharedSecretEvent
				}
			}
			if card, ok := panda.ParseCard(cardText); ok && stack.Add(card) {
				point := nextPoint
				if l := len(freeList); l > 0 {
					point = freeList[l-1]
					freeList = freeList[:l-1]
				} else {
					nextPoint.col++
					if nextPoint.col == cardsPerRow {
						nextPoint.row++
						nextPoint.col = 0
					}
				}
				markup := card.String()
				if card.IsRed() {
					numLen := 1
					if markup[0] == '1' {
						numLen = 2
					}
					markup = markup[:numLen] + "<span color=\"red\">" + markup[numLen:] + "</span>"
				}
				name := fmt.Sprintf("card-%d,%d", point.col, point.row)
				c.ui.Actions() <- GridSet{"cards", point.col, point.row, Button{
					widgetBase: widgetBase{name: name},
					markup:     markup,
				}}
				cardAtLocation[point] = card
				if min := stack.MinimumDecks(); min > minDecks {
					minDecks = min
					if min > 1 {
						c.ui.Actions() <- Sensitive{name: "numdecks", sensitive: false}
					}
				}
			}
			c.ui.Actions() <- SetEntry{name: "cardentry", text: update.text[len(cardText):]}
			c.ui.Signal()
			continue
		}

		if click, ok := event.(Click); ok {
			switch {
			case strings.HasPrefix(click.name, "card-"):
				var point gridPoint
				fmt.Sscanf(click.name[5:], "%d,%d", &point.col, &point.row)
				card := cardAtLocation[point]
				freeList = append(freeList, point)
				delete(cardAtLocation, point)
				stack.Remove(card)
				if min := stack.MinimumDecks(); min < minDecks {
					minDecks = min
					if min < 2 {
						c.ui.Actions() <- Sensitive{name: "numdecks", sensitive: true}
					}
				}
				c.ui.Actions() <- Destroy{name: click.name}
				c.ui.Signal()
			case click.name == "hastime":
				timeEnabled = click.checks["hastime"]
				c.ui.Actions() <- Sensitive{name: "cal", sensitive: timeEnabled}
				c.ui.Actions() <- Sensitive{name: "hour", sensitive: timeEnabled}
				c.ui.Actions() <- Sensitive{name: "minute", sensitive: timeEnabled}
				c.ui.Signal()
			case click.name == "numdecks":
				numDecks := click.radios["numdecks"] + 1
				if numDecks >= minDecks {
					stack.NumDecks = numDecks
				}
			case click.name == "begin":
				secret := panda.SharedSecret{
					Secret: click.entries["shared"],
					Cards:  *stack,
				}
				if timeEnabled {
					date := click.calendars["cal"]
					secret.Year = date.year
					secret.Month = date.month
					secret.Day = date.day
					secret.Hours = click.spinButtons["hour"]
					secret.Minutes = click.spinButtons["minute"]
				}
				mp := c.newMeetingPlace()
				kx, err := panda.NewKeyExchange(c.rand, mp, &secret, contact.kxsBytes)
				if err != nil {
					panic(err)
				}
				kx.Testing = c.testing
				contact.pandaKeyExchange = kx.Marshal()
				contact.kxsBytes = nil
				break SharedSecretEvent
			}
		}
	}

	c.save()
	c.pandaWaitGroup.Add(1)
	go c.runPANDA(contact.pandaKeyExchange, contact.id, contact.name)
	return c.showContact(contact.id)
}