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() *model.Profile } // 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) 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() 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 } 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 _, profile := range cp.Contacts { ss := NewStreamStore(ps.directory, profile.LocalID, ps.password) profile.Timeline.SetMessages(ss.Read()) ps.streamStores[profile.Onion] = ss } for _, group := range cp.Groups { ss := NewStreamStore(ps.directory, group.LocalID, ps.password) group.Timeline.SetMessages(ss.Read()) ps.streamStores[group.GroupID] = ss } } return err } func (ps *profileStore) GetProfileCopy() *model.Profile { return ps.profile.GetCopy() } func (ps *profileStore) eventHandler() { for { ev := ps.queue.Next() switch ev.EventType { case event.BlockPeer: contact, exists := ps.profile.GetContact(ev.Data["Onion"]) 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) 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) } default: return } } } func (ps *profileStore) Shutdown() { if ps.queue != nil { ps.queue.Shutdown() } }