package storage import ( "cwtch.im/cwtch/event" "cwtch.im/cwtch/model" "cwtch.im/cwtch/protocol" "encoding/json" "git.openprivacy.ca/openprivacy/libricochet-go/log" "github.com/golang/protobuf/proto" "os" "time" ) const profileFilename = "profile" type profileStore struct { fs FileStore streamStores map[string]StreamStore directory string password string profile *model.Profile eventManager event.Manager queue *event.Queue writer bool } // ProfileStore is an interface to managing the storage of Cwtch Profiles type ProfileStore interface { Load() error Shutdown() GetProfileCopy(timeline bool) *model.Profile GetNewPeerMessage() *event.Event GetStatusMessages() []*event.Event } // NewProfileWriterStore returns a profile store backed by a filestore listening for events and saving them // directory should be $appDir/profiles/$rand func NewProfileWriterStore(eventManager event.Manager, directory, password string, profile *model.Profile) ProfileStore { os.Mkdir(directory, 0700) ps := &profileStore{fs: NewFileStore(directory, profileFilename, password), password: password, directory: directory, profile: profile, eventManager: eventManager, streamStores: map[string]StreamStore{}, writer: true} ps.queue = event.NewEventQueue(100) if profile != nil { ps.save() } go ps.eventHandler() ps.eventManager.Subscribe(event.BlockPeer, ps.queue.EventChannel) ps.eventManager.Subscribe(event.PeerCreated, ps.queue.EventChannel) ps.eventManager.Subscribe(event.GroupCreated, ps.queue.EventChannel) ps.eventManager.Subscribe(event.SetProfileName, ps.queue.EventChannel) ps.eventManager.Subscribe(event.SetAttribute, ps.queue.EventChannel) ps.eventManager.Subscribe(event.SetPeerAttribute, ps.queue.EventChannel) ps.eventManager.Subscribe(event.SetGroupAttribute, ps.queue.EventChannel) ps.eventManager.Subscribe(event.AcceptGroupInvite, ps.queue.EventChannel) ps.eventManager.Subscribe(event.NewGroupInvite, ps.queue.EventChannel) ps.eventManager.Subscribe(event.NewMessageFromGroup, ps.queue.EventChannel) ps.eventManager.Subscribe(event.PeerStateChange, ps.queue.EventChannel) ps.eventManager.Subscribe(event.ServerStateChange, ps.queue.EventChannel) ps.eventManager.Subscribe(event.DeleteContact, ps.queue.EventChannel) ps.eventManager.Subscribe(event.DeleteGroup, ps.queue.EventChannel) return ps } // ReadProfile reads a profile from storqage and returns the profile // directory should be $appDir/profiles/$rand func ReadProfile(directory, password string) (*model.Profile, error) { os.Mkdir(directory, 0700) ps := &profileStore{fs: NewFileStore(directory, profileFilename, password), password: password, directory: directory, profile: nil, eventManager: nil, streamStores: map[string]StreamStore{}, writer: true} err := ps.Load() if err != nil { return nil, err } profile := ps.GetProfileCopy(true) return profile, nil } // NewProfile creates a new profile for use in the profile store. func NewProfile(name string) *model.Profile { profile := model.GenerateNewProfile(name) return profile } // GetNewPeerMessage is for AppService to call on Reload events, to reseed the AppClient with the loaded peers func (ps *profileStore) GetNewPeerMessage() *event.Event { message := event.NewEventList(event.NewPeer, event.Identity, ps.profile.LocalID, event.Password, ps.password, event.Status, "running") return &message } func (ps *profileStore) GetStatusMessages() []*event.Event { messages := []*event.Event{} for _, contact := range ps.profile.Contacts { message := event.NewEvent(event.PeerStateChange, map[event.Field]string{ event.RemotePeer: string(contact.Onion), event.ConnectionState: contact.State, }) messages = append(messages, &message) } doneServers := make(map[string]bool) for _, group := range ps.profile.Groups { if _, exists := doneServers[group.GroupServer]; !exists { message := event.NewEvent(event.ServerStateChange, map[event.Field]string{ event.GroupServer: string(group.GroupServer), event.ConnectionState: group.State, }) messages = append(messages, &message) doneServers[group.GroupServer] = true } } return messages } func (ps *profileStore) save() error { if ps.writer { bytes, _ := json.Marshal(ps.profile) return ps.fs.Save(bytes) } return nil } // Load instantiates a cwtchPeer from the file store func (ps *profileStore) Load() error { decrypted, err := ps.fs.Load() if err != nil { return err } cp := new(model.Profile) err = json.Unmarshal(decrypted, &cp) if err == nil { ps.profile = cp for gid, group := range cp.Groups { ss := NewStreamStore(ps.directory, group.LocalID, ps.password) cp.Groups[gid].Timeline.SetMessages(ss.Read()) ps.streamStores[group.GroupID] = ss } } return err } func (ps *profileStore) GetProfileCopy(timeline bool) *model.Profile { return ps.profile.GetCopy(timeline) } func (ps *profileStore) eventHandler() { for { ev := ps.queue.Next() switch ev.EventType { case event.BlockPeer: contact, exists := ps.profile.GetContact(ev.Data[event.RemotePeer]) if exists { contact.Blocked = true ps.save() } case event.PeerCreated: var pp *model.PublicProfile json.Unmarshal([]byte(ev.Data[event.Data]), &pp) ps.profile.AddContact(ev.Data[event.RemotePeer], pp) // TODO: configure - allow peers to be configured to turn on limited storage /*ss := NewStreamStore(ps.directory, pp.LocalID, ps.password) pp.Timeline.SetMessages(ss.Read()) ps.streamStores[pp.Onion] = ss ps.save()*/ case event.GroupCreated: var group *model.Group json.Unmarshal([]byte(ev.Data[event.Data]), &group) ps.profile.AddGroup(group) ps.streamStores[group.GroupID] = NewStreamStore(ps.directory, group.LocalID, ps.password) ps.save() case event.SetProfileName: ps.profile.Name = ev.Data[event.ProfileName] ps.profile.SetAttribute("name", ev.Data[event.ProfileName]) ps.save() case event.SetAttribute: ps.profile.SetAttribute(ev.Data[event.Key], ev.Data[event.Data]) ps.save() case event.SetPeerAttribute: contact, exists := ps.profile.GetContact(ev.Data[event.RemotePeer]) if exists { contact.SetAttribute(ev.Data[event.Key], ev.Data[event.Data]) ps.save() } else { log.Errorf("error setting attribute on peer %v peer does not exist", ev) } case event.SetGroupAttribute: group := ps.profile.GetGroupByGroupID(ev.Data[event.GroupID]) if group != nil { group.SetAttribute(ev.Data[event.Key], ev.Data[event.Data]) ps.save() } else { log.Errorf("error setting attribute on group %v group does not exist", ev) } case event.AcceptGroupInvite: err := ps.profile.AcceptInvite(ev.Data[event.GroupID]) if err == nil { ps.save() } else { log.Errorf("error accepting group invite") } case event.NewGroupInvite: var gci protocol.GroupChatInvite err := proto.Unmarshal([]byte(ev.Data[event.GroupInvite]), &gci) if err == nil { ps.profile.ProcessInvite(&gci, ev.Data[event.RemotePeer]) ps.save() group := ps.profile.Groups[gci.GetGroupName()] ps.streamStores[group.GroupID] = NewStreamStore(ps.directory, group.LocalID, ps.password) } else { log.Errorf("error storing new group invite: %v %v", ev, err) } case event.NewMessageFromGroup: groupid := ev.Data[event.GroupID] received, _ := time.Parse(time.RFC3339Nano, ev.Data[event.TimestampReceived]) sent, _ := time.Parse(time.RFC3339Nano, ev.Data[event.TimestampSent]) message := model.Message{Received: received, Timestamp: sent, Message: ev.Data[event.Data], PeerID: ev.Data[event.RemotePeer], Signature: []byte(ev.Data[event.Signature]), PreviousMessageSig: []byte(ev.Data[event.PreviousSignature])} ss, exists := ps.streamStores[groupid] if exists { ss.Write(message) } else { log.Errorf("error storing new group message: %v stream store does not exist", ev) } case event.PeerStateChange: if _, exists := ps.profile.Contacts[ev.Data[event.RemotePeer]]; exists { ps.profile.Contacts[ev.Data[event.RemotePeer]].State = ev.Data[event.ConnectionState] } case event.ServerStateChange: for _, group := range ps.profile.Groups { if group.GroupServer == ev.Data[event.GroupServer] { group.State = ev.Data[event.ConnectionState] } } case event.DeleteContact: onion := ev.Data[event.RemotePeer] ps.profile.DeleteContact(onion) ps.save() case event.DeleteGroup: groupID := ev.Data[event.GroupID] ps.profile.DeleteGroup(groupID) ps.save() ss, exists := ps.streamStores[groupID] if exists { ss.Delete() delete(ps.streamStores, groupID) } default: return } } } func (ps *profileStore) Shutdown() { if ps.queue != nil { ps.queue.Shutdown() } }