package peer import ( "cwtch.im/cwtch/event" "cwtch.im/cwtch/model" "cwtch.im/cwtch/model/attr" "cwtch.im/cwtch/protocol/connections" "encoding/base32" "encoding/base64" "encoding/json" "errors" "git.openprivacy.ca/openprivacy/log" "strconv" "strings" "sync" "time" ) var autoHandleableEvents = map[event.Type]bool{event.EncryptedGroupMessage: true, event.PeerStateChange: true, event.ServerStateChange: true, event.NewGroupInvite: true, event.NewMessageFromPeer: true, event.PeerAcknowledgement: true, event.PeerError: true, event.SendMessageToGroupError: true, event.NewGetValMessageFromPeer: true, event.NewRetValMessageFromPeer: true, event.ProtocolEngineStopped: true, event.RetryServerRequest: true} // DefaultEventsToHandle specifies which events will be subscribed to // when a peer has its Init() function called var DefaultEventsToHandle = []event.Type{ event.EncryptedGroupMessage, event.NewMessageFromPeer, event.PeerAcknowledgement, event.NewGroupInvite, event.PeerError, event.SendMessageToGroupError, event.NewGetValMessageFromPeer, event.ProtocolEngineStopped, event.RetryServerRequest, } // cwtchPeer manages incoming and outgoing connections and all processing for a Cwtch cwtchPeer type cwtchPeer struct { Profile *model.Profile mutex sync.Mutex shutdown bool listenStatus bool queue event.Queue eventBus event.Manager } // BlockUnknownConnections will auto disconnect from connections if authentication doesn't resolve a hostname // known to peer. func (cp *cwtchPeer) BlockUnknownConnections() { cp.eventBus.Publish(event.NewEvent(event.BlockUnknownPeers, map[event.Field]string{})) } // AllowUnknownConnections will permit connections from unknown contacts. func (cp *cwtchPeer) AllowUnknownConnections() { cp.eventBus.Publish(event.NewEvent(event.AllowUnknownPeers, map[event.Field]string{})) } // ReadContacts is a meta-interface intended to restrict callers to read-only access to contacts type ReadContacts interface { GetContacts() []string GetContact(string) *model.PublicProfile GetContactAttribute(string, string) (string, bool) } // ModifyContacts is a meta-interface intended to restrict callers to modify-only access to contacts type ModifyContacts interface { AddContact(nick, onion string, authorization model.Authorization) SetContactAuthorization(string, model.Authorization) error SetContactAttribute(string, string, string) DeleteContact(string) } // AccessPeeringState provides access to functions relating to the underlying connections of a peer. type AccessPeeringState interface { GetPeerState(string) (connections.ConnectionState, bool) } // ModifyPeeringState is a meta-interface intended to restrict callers to modify-only access to connection peers type ModifyPeeringState interface { BlockUnknownConnections() AllowUnknownConnections() PeerWithOnion(string) JoinServer(string) error } // ModifyContactsAndPeers is a meta-interface intended to restrict a call to reading and modifying contacts // and peers. type ModifyContactsAndPeers interface { ReadContacts ModifyContacts ModifyPeeringState } // ReadServers provides access to the servers type ReadServers interface { GetServers() []string } // ReadGroups provides read-only access to group state type ReadGroups interface { GetGroup(string) *model.Group GetGroupState(string) (connections.ConnectionState, bool) GetGroups() []string GetGroupAttribute(string, string) (string, bool) ExportGroup(string) (string, error) } // ModifyGroups provides write-only access add/edit/remove new groups type ModifyGroups interface { ImportGroup(string) (string, error) StartGroup(string) (string, string, error) AcceptInvite(string) error RejectInvite(string) DeleteGroup(string) SetGroupAttribute(string, string, string) } // ModifyServers provides write-only access to servers type ModifyServers interface { AddServer(string) error } // SendMessages enables a caller to sender messages to a contact // Note: type SendMessages interface { SendGetValToPeer(string, string, string) SendMessageToPeer(string, string) string // TODO This should probably not be exposed StoreMessage(onion string, messageTxt string, sent time.Time) // TODO Extract once groups are stable InviteOnionToGroup(string, string) error } // SendMessagesToGroup enables a caller to sender messages to a group type SendMessagesToGroup interface { SendMessageToGroupTracked(string, string) (string, error) } // CwtchPeer provides us with a way of testing systems built on top of cwtch without having to // directly implement a cwtchPeer. type CwtchPeer interface { // Core Cwtch Peer Functions that should not be exposed to // most functions Init(event.Manager) AutoHandleEvents(events []event.Type) Listen() StartPeersConnections() StartServerConnections() Shutdown() // Relating to local attributes GetOnion() string SetAttribute(string, string) GetAttribute(string) (string, bool) ReadContacts ModifyContacts AccessPeeringState ModifyPeeringState ReadGroups ModifyGroups ReadServers ModifyServers SendMessages SendMessagesToGroup } // NewCwtchPeer creates and returns a new cwtchPeer with the given name. func NewCwtchPeer(name string) CwtchPeer { cp := new(cwtchPeer) cp.Profile = model.GenerateNewProfile(name) cp.shutdown = false return cp } // FromProfile generates a new peer from a profile. func FromProfile(profile *model.Profile) CwtchPeer { cp := new(cwtchPeer) cp.Profile = profile cp.shutdown = false return cp } // Init instantiates a cwtchPeer func (cp *cwtchPeer) Init(eventBus event.Manager) { cp.InitForEvents(eventBus, DefaultEventsToHandle) } func (cp *cwtchPeer) InitForEvents(eventBus event.Manager, toBeHandled []event.Type) { cp.queue = event.NewQueue() go cp.eventHandler() cp.eventBus = eventBus cp.AutoHandleEvents(toBeHandled) } // AutoHandleEvents sets an event (if able) to be handled by this peer func (cp *cwtchPeer) AutoHandleEvents(events []event.Type) { for _, ev := range events { if _, exists := autoHandleableEvents[ev]; exists { cp.eventBus.Subscribe(ev, cp.queue) } else { log.Errorf("Peer asked to autohandle event it cannot: %v\n", ev) } } } // ImportGroup initializes a group from an imported source rather than a peer invite func (cp *cwtchPeer) ImportGroup(exportedInvite string) (string, error) { cp.mutex.Lock() defer cp.mutex.Unlock() gid, err := cp.Profile.ProcessInvite(exportedInvite) if err == nil { cp.eventBus.Publish(event.NewEvent(event.NewGroup, map[event.Field]string{event.GroupID: gid, event.GroupInvite: exportedInvite})) } return gid, err } // ExportGroup serializes a group invite so it can be given offline func (cp *cwtchPeer) ExportGroup(groupID string) (string, error) { cp.mutex.Lock() defer cp.mutex.Unlock() group := cp.Profile.GetGroup(groupID) if group != nil { return group.Invite() } return "", errors.New("group id could not be found") } // StartGroup create a new group linked to the given server and returns the group ID, an invite or an error. func (cp *cwtchPeer) StartGroup(server string) (string, string, error) { cp.mutex.Lock() groupID, invite, err := cp.Profile.StartGroup(server) cp.mutex.Unlock() if err == nil { group := cp.GetGroup(groupID) jsobj, err := json.Marshal(group) if err == nil { cp.eventBus.Publish(event.NewEvent(event.GroupCreated, map[event.Field]string{ event.Data: string(jsobj), })) } } else { log.Errorf("error creating group: %v", err) } return groupID, invite, err } // GetGroups returns an unordered list of all group IDs. func (cp *cwtchPeer) GetGroups() []string { cp.mutex.Lock() defer cp.mutex.Unlock() return cp.Profile.GetGroups() } // GetGroup returns a pointer to a specific group, nil if no group exists. func (cp *cwtchPeer) GetGroup(groupID string) *model.Group { cp.mutex.Lock() defer cp.mutex.Unlock() return cp.Profile.GetGroup(groupID) } func (cp *cwtchPeer) AddContact(nick, onion string, authorization model.Authorization) { decodedPub, _ := base32.StdEncoding.DecodeString(strings.ToUpper(onion)) pp := &model.PublicProfile{Name: nick, Ed25519PublicKey: decodedPub, Authorization: authorization, Onion: onion, Attributes: map[string]string{"nick": nick}} cp.Profile.AddContact(onion, pp) pd, _ := json.Marshal(pp) cp.eventBus.Publish(event.NewEvent(event.PeerCreated, map[event.Field]string{ event.Data: string(pd), event.RemotePeer: onion, })) cp.eventBus.Publish(event.NewEventList(event.SetPeerAuthorization, event.RemotePeer, onion, event.Authorization, string(authorization))) // Default to Deleting Peer History cp.eventBus.Publish(event.NewEventList(event.SetPeerAttribute, event.RemotePeer, onion, event.SaveHistoryKey, event.DeleteHistoryDefault)) } // AddServer takes in a serialized server specification (a bundle of related keys) and adds a contact for the // server assuming there are no errors and the contact doesn't already exist. // TODO in the future this function should also integrate with a trust provider to validate the key bundle. func (cp *cwtchPeer) AddServer(serverSpecification string) error { // This confirms that the server did at least sign the bundle keyBundle, err := model.DeserializeAndVerify([]byte(serverSpecification)) if err != nil { return err } log.Debugf("Got new key bundle %v", keyBundle.Serialize()) // TODO if the key bundle is incomplete then error out. In the future we may allow servers to attest to new // keys or subsets of keys, but for now they must commit only to a complete set of keys required for Cwtch Groups // (that way we can be assured that the keybundle we store is a valid one) if keyBundle.HasKeyType(model.KeyTypeTokenOnion) == false || keyBundle.HasKeyType(model.KeyTypeServerOnion) == false || keyBundle.HasKeyType(model.KeyTypePrivacyPass) == false { return errors.New("keybundle is incomplete") } if keyBundle.HasKeyType(model.KeyTypeServerOnion) { onionKey, _ := keyBundle.GetKey(model.KeyTypeServerOnion) onion := string(onionKey) // Add the contact if we don't already have it if cp.GetContact(onion) == nil { decodedPub, _ := base32.StdEncoding.DecodeString(strings.ToUpper(onion)) ab := keyBundle.AttributeBundle() pp := &model.PublicProfile{Name: onion, Ed25519PublicKey: decodedPub, Authorization: model.AuthUnknown, Onion: onion, Attributes: ab} // The only part of this function that actually modifies the profile... cp.mutex.Lock() cp.Profile.AddContact(onion, pp) cp.mutex.Unlock() pd, _ := json.Marshal(pp) cp.eventBus.Publish(event.NewEvent(event.PeerCreated, map[event.Field]string{ event.Data: string(pd), event.RemotePeer: onion, })) } // At this point we know the server exists server := cp.GetContact(onion) ab := keyBundle.AttributeBundle() // Check server bundle for consistency if we have different keys stored than in the tofu bundle then we // abort... for k, v := range ab { val, exists := server.GetAttribute(k) if exists { if val != v { // this is inconsistent! return model.InconsistentKeyBundleError } } // we haven't seen this key associated with the server before } // Store the key bundle for the server so we can reconstruct a tofubundle invite cp.SetContactAttribute(onion, string(model.BundleType), serverSpecification) // If we have gotten to this point we can assume this is a safe key bundle signed by the // server with no conflicting keys. So we are going to publish all the keys for k, v := range ab { log.Debugf("Server (%v) has %v key %v", onion, k, v) cp.SetContactAttribute(onion, k, v) } return nil } return err } // GetContacts returns an unordered list of onions func (cp *cwtchPeer) GetContacts() []string { cp.mutex.Lock() defer cp.mutex.Unlock() return cp.Profile.GetContacts() } // GetServers returns an unordered list of servers func (cp *cwtchPeer) GetServers() []string { contacts := cp.Profile.GetContacts() var servers []string for _, contact := range contacts { if cp.GetContact(contact).IsServer() { servers = append(servers, contact) } } return servers } // GetContact returns a given contact, nil is no such contact exists func (cp *cwtchPeer) GetContact(onion string) *model.PublicProfile { cp.mutex.Lock() defer cp.mutex.Unlock() contact, _ := cp.Profile.GetContact(onion) return contact } func (cp *cwtchPeer) GetOnion() string { cp.mutex.Lock() defer cp.mutex.Unlock() return cp.Profile.Onion } func (cp *cwtchPeer) GetPeerState(onion string) (connections.ConnectionState, bool) { cp.mutex.Lock() defer cp.mutex.Unlock() if peer, ok := cp.Profile.Contacts[onion]; ok { return connections.ConnectionStateToType()[peer.State], true } return connections.DISCONNECTED, false } func (cp *cwtchPeer) GetGroupState(groupid string) (connections.ConnectionState, bool) { cp.mutex.Lock() defer cp.mutex.Unlock() if group, ok := cp.Profile.Groups[groupid]; ok { return connections.ConnectionStateToType()[group.State], true } return connections.DISCONNECTED, false } // PeerWithOnion is the entry point for cwtchPeer relationships func (cp *cwtchPeer) PeerWithOnion(onion string) { cp.mutex.Lock() defer cp.mutex.Unlock() if _, exists := cp.Profile.GetContact(onion); !exists { cp.AddContact(onion, onion, model.AuthApproved) } cp.eventBus.Publish(event.NewEvent(event.PeerRequest, map[event.Field]string{event.RemotePeer: onion})) } // DeleteContact deletes a peer from the profile, storage, and handling func (cp *cwtchPeer) DeleteContact(onion string) { cp.mutex.Lock() cp.Profile.DeleteContact(onion) defer cp.mutex.Unlock() cp.eventBus.Publish(event.NewEventList(event.DeleteContact, event.RemotePeer, onion)) } // DeleteGroup deletes a Group from the profile, storage, and handling func (cp *cwtchPeer) DeleteGroup(groupID string) { cp.mutex.Lock() cp.Profile.DeleteGroup(groupID) defer cp.mutex.Unlock() cp.eventBus.Publish(event.NewEventList(event.DeleteGroup, event.GroupID, groupID)) } // InviteOnionToGroup kicks off the invite process func (cp *cwtchPeer) InviteOnionToGroup(onion string, groupid string) error { cp.mutex.Lock() group := cp.Profile.GetGroup(groupid) if group == nil { cp.mutex.Unlock() return errors.New("invalid group id") } invite, err := group.Invite() cp.mutex.Unlock() if err == nil { cp.SendMessageToPeer(onion, invite) } return err } // JoinServer manages a new server connection with the given onion address func (cp *cwtchPeer) JoinServer(onion string) error { if cp.GetContact(onion) != nil { tokenY, yExists := cp.GetContact(onion).GetAttribute(string(model.KeyTypePrivacyPass)) tokenOnion, onionExists := cp.GetContact(onion).GetAttribute(string(model.KeyTypeTokenOnion)) if yExists && onionExists { messages := cp.GetContact(onion).Timeline.GetMessages() // by default we do a full sync signature := base64.StdEncoding.EncodeToString([]byte{}) // if we know of messages on the server, we only ask to receive newer messages if len(messages) > 0 { lastMessageIndex := len(messages) - 1 signature = base64.StdEncoding.EncodeToString(messages[lastMessageIndex].Signature) } cp.eventBus.Publish(event.NewEvent(event.JoinServer, map[event.Field]string{event.GroupServer: onion, event.ServerTokenY: tokenY, event.ServerTokenOnion: tokenOnion, event.Signature: signature})) return nil } } return errors.New("no keys found for server connection") } // SendMessageToGroupTracked attempts to sent the given message to the given group id. // It returns the signature of the message which can be used to identify it in any UX layer. func (cp *cwtchPeer) SendMessageToGroupTracked(groupid string, message string) (string, error) { cp.mutex.Lock() group := cp.Profile.GetGroup(groupid) defer cp.mutex.Unlock() if group == nil { return "", errors.New("invalid group id") } ct, sig, err := cp.Profile.EncryptMessageToGroup(message, groupid) if err == nil { cp.eventBus.Publish(event.NewEvent(event.SendMessageToGroup, map[event.Field]string{event.GroupServer: group.GroupServer, event.Ciphertext: base64.StdEncoding.EncodeToString(ct), event.Signature: base64.StdEncoding.EncodeToString(sig)})) } return string(sig), err } func (cp *cwtchPeer) SendMessageToPeer(onion string, message string) string { event := event.NewEvent(event.SendMessageToPeer, map[event.Field]string{event.RemotePeer: onion, event.Data: message}) cp.mutex.Lock() contact, _ := cp.Profile.GetContact(onion) event.EventID = strconv.Itoa(contact.Timeline.Len()) cp.Profile.AddSentMessageToContactTimeline(onion, message, time.Now(), event.EventID) cp.mutex.Unlock() cp.eventBus.Publish(event) return event.EventID } func (cp *cwtchPeer) SendGetValToPeer(onion string, scope string, path string) { event := event.NewEventList(event.SendGetValMessageToPeer, event.RemotePeer, onion, event.Scope, scope, event.Path, path) cp.eventBus.Publish(event) } // BlockPeer blocks an existing peer relationship. func (cp *cwtchPeer) SetContactAuthorization(peer string, authorization model.Authorization) error { cp.mutex.Lock() err := cp.Profile.SetContactAuthorization(peer, authorization) cp.mutex.Unlock() cp.eventBus.Publish(event.NewEvent(event.SetPeerAuthorization, map[event.Field]string{event.RemotePeer: peer, event.Authorization: string(authorization)})) return err } // AcceptInvite accepts a given existing group invite func (cp *cwtchPeer) AcceptInvite(groupID string) error { cp.mutex.Lock() err := cp.Profile.AcceptInvite(groupID) cp.mutex.Unlock() if err != nil { return err } cp.eventBus.Publish(event.NewEvent(event.AcceptGroupInvite, map[event.Field]string{event.GroupID: groupID})) cp.JoinServer(cp.Profile.Groups[groupID].GroupServer) return nil } // RejectInvite rejects a given group invite. func (cp *cwtchPeer) RejectInvite(groupID string) { cp.mutex.Lock() defer cp.mutex.Unlock() cp.Profile.RejectInvite(groupID) cp.eventBus.Publish(event.NewEvent(event.RejectGroupInvite, map[event.Field]string{event.GroupID: groupID})) } // Listen makes the peer open a listening port to accept incoming connections (and be detactably online) func (cp *cwtchPeer) Listen() { cp.mutex.Lock() defer cp.mutex.Unlock() if cp.listenStatus == false { log.Infof("cwtchPeer Listen sending ProtocolEngineStartListen\n") cp.listenStatus = true cp.eventBus.Publish(event.NewEvent(event.ProtocolEngineStartListen, map[event.Field]string{event.Onion: cp.Profile.Onion})) } else { // protocol engine is already listening } } // StartPeersConnections attempts to connect to peer connections func (cp *cwtchPeer) StartPeersConnections() { for _, contact := range cp.GetContacts() { if cp.GetContact(contact).IsServer() == false { cp.PeerWithOnion(contact) } } } // StartServerConnections attempts to connect to all server connections func (cp *cwtchPeer) StartServerConnections() { for _, contact := range cp.GetContacts() { if cp.GetContact(contact).IsServer() { cp.JoinServer(contact) } } } // SetAttribute sets an attribute for this profile and emits an event func (cp *cwtchPeer) SetAttribute(key string, val string) { cp.mutex.Lock() cp.Profile.SetAttribute(key, val) defer cp.mutex.Unlock() cp.eventBus.Publish(event.NewEvent(event.SetAttribute, map[event.Field]string{ event.Key: key, event.Data: val, })) } // GetAttribute gets an attribute for the profile func (cp *cwtchPeer) GetAttribute(key string) (string, bool) { cp.mutex.Lock() defer cp.mutex.Unlock() if val, exists := cp.Profile.GetAttribute(key); exists { return val, true } if key == attr.GetLocalScope("name") { return cp.Profile.Name, true } return "", false } // SetContactAttribute sets an attribute for the indicated contact and emits an event func (cp *cwtchPeer) SetContactAttribute(onion string, key string, val string) { cp.mutex.Lock() defer cp.mutex.Unlock() if contact, ok := cp.Profile.GetContact(onion); ok { contact.SetAttribute(key, val) cp.eventBus.Publish(event.NewEvent(event.SetPeerAttribute, map[event.Field]string{ event.RemotePeer: onion, event.Key: key, event.Data: val, })) } } // GetContactAttribute gets an attribute for the indicated contact func (cp *cwtchPeer) GetContactAttribute(onion string, key string) (string, bool) { cp.mutex.Lock() defer cp.mutex.Unlock() if contact, ok := cp.Profile.GetContact(onion); ok { if val, exists := contact.GetAttribute(key); exists { return val, true } } return "", false } // SetGroupAttribute sets an attribute for the indicated group and emits an event func (cp *cwtchPeer) SetGroupAttribute(gid string, key string, val string) { cp.mutex.Lock() defer cp.mutex.Unlock() if group := cp.Profile.GetGroup(gid); group != nil { group.SetAttribute(key, val) cp.eventBus.Publish(event.NewEvent(event.SetGroupAttribute, map[event.Field]string{ event.GroupID: gid, event.Key: key, event.Data: val, })) } } // GetGroupAttribute gets an attribute for the indicated group func (cp *cwtchPeer) GetGroupAttribute(gid string, key string) (string, bool) { cp.mutex.Lock() defer cp.mutex.Unlock() if group := cp.Profile.GetGroup(gid); group != nil { if val, exists := group.GetAttribute(key); exists { return val, true } } return "", false } // Shutdown kills all connections and cleans up all goroutines for the peer func (cp *cwtchPeer) Shutdown() { cp.mutex.Lock() defer cp.mutex.Unlock() cp.shutdown = true cp.queue.Shutdown() } func (cp *cwtchPeer) StoreMessage(onion string, messageTxt string, sent time.Time) { if cp.GetContact(onion) == nil { cp.AddContact(onion, onion, model.AuthUnknown) } cp.mutex.Lock() cp.Profile.AddMessageToContactTimeline(onion, messageTxt, sent) cp.mutex.Unlock() } // eventHandler process events from other subsystems func (cp *cwtchPeer) eventHandler() { for { ev := cp.queue.Next() switch ev.EventType { /***** Default auto handled events *****/ case event.ProtocolEngineStopped: cp.mutex.Lock() cp.listenStatus = false log.Infof("Protocol engine for %v has stopped listening", cp.Profile.Onion) cp.mutex.Unlock() case event.EncryptedGroupMessage: // If successful, a side effect is the message is added to the group's timeline cp.mutex.Lock() ciphertext, _ := base64.StdEncoding.DecodeString(ev.Data[event.Ciphertext]) signature, _ := base64.StdEncoding.DecodeString(ev.Data[event.Signature]) ok, groupID, message, seen := cp.Profile.AttemptDecryption(ciphertext, signature) cp.mutex.Unlock() if ok && !seen { cp.eventBus.Publish(event.NewEvent(event.NewMessageFromGroup, map[event.Field]string{event.TimestampReceived: message.Received.Format(time.RFC3339Nano), event.TimestampSent: message.Timestamp.Format(time.RFC3339Nano), event.Data: message.Message, event.GroupID: groupID, event.Signature: base64.StdEncoding.EncodeToString(message.Signature), event.PreviousSignature: base64.StdEncoding.EncodeToString(message.PreviousMessageSig), event.RemotePeer: message.PeerID})) } case event.NewMessageFromPeer: //event.TimestampReceived, event.RemotePeer, event.Data ts, _ := time.Parse(time.RFC3339Nano, ev.Data[event.TimestampReceived]) cp.StoreMessage(ev.Data[event.RemotePeer], ev.Data[event.Data], ts) case event.PeerAcknowledgement: cp.mutex.Lock() idx := cp.Profile.AckSentMessageToPeer(ev.Data[event.RemotePeer], ev.Data[event.EventID]) edata := ev.Data edata[event.Index] = strconv.Itoa(idx) cp.eventBus.Publish(event.NewEvent(event.IndexedAcknowledgement, edata)) cp.mutex.Unlock() case event.SendMessageToGroupError: cp.mutex.Lock() cp.Profile.AddGroupSentMessageError(ev.Data[event.GroupServer], ev.Data[event.Signature], ev.Data[event.Error]) cp.mutex.Unlock() case event.SendMessageToPeerError: cp.mutex.Lock() cp.Profile.ErrorSentMessageToPeer(ev.Data[event.RemotePeer], ev.Data[event.EventID], ev.Data[event.Error]) cp.mutex.Unlock() case event.RetryServerRequest: // Automated Join Server Request triggered by a plugin. log.Infof("profile received an automated retry event for %v", ev.Data[event.GroupServer]) cp.JoinServer(ev.Data[event.GroupServer]) case event.NewGetValMessageFromPeer: onion := ev.Data[event.RemotePeer] scope := ev.Data[event.Scope] path := ev.Data[event.Path] log.Debugf("NewGetValMessageFromPeer for %v%v from %v\n", scope, path, onion) remotePeer := cp.GetContact(onion) if remotePeer != nil && remotePeer.Authorization == model.AuthApproved { if scope == attr.PublicScope { val, exists := cp.GetAttribute(attr.GetPublicScope(path)) resp := event.NewEvent(event.SendRetValMessageToPeer, map[event.Field]string{event.RemotePeer: onion, event.Exists: strconv.FormatBool(exists)}) resp.EventID = ev.EventID if exists { resp.Data[event.Data] = val } else { resp.Data[event.Data] = "" } log.Debugf("Responding with SendRetValMessageToPeer exists:%v data: %v\n", exists, val) cp.eventBus.Publish(resp) } } /***** Non default but requestable handlable events *****/ case event.NewRetValMessageFromPeer: onion := ev.Data[event.RemotePeer] scope := ev.Data[event.Scope] path := ev.Data[event.Path] val := ev.Data[event.Data] exists, _ := strconv.ParseBool(ev.Data[event.Exists]) log.Debugf("NewRetValMessageFromPeer %v %v%v %v %v\n", onion, scope, path, exists, val) if exists { if scope == attr.PublicScope { cp.SetContactAttribute(onion, attr.GetPeerScope(path), val) } } case event.PeerStateChange: cp.mutex.Lock() if _, exists := cp.Profile.Contacts[ev.Data[event.RemotePeer]]; exists { cp.Profile.Contacts[ev.Data[event.RemotePeer]].State = ev.Data[event.ConnectionState] } cp.mutex.Unlock() case event.ServerStateChange: cp.mutex.Lock() for _, group := range cp.Profile.Groups { if group.GroupServer == ev.Data[event.GroupServer] { group.State = ev.Data[event.ConnectionState] } } cp.mutex.Unlock() default: if ev.EventType != "" { log.Errorf("peer event handler received an event it was not subscribed for: %v", ev.EventType) } return } } }