package filesharing import ( "cwtch.im/cwtch/event" "cwtch.im/cwtch/model" "cwtch.im/cwtch/model/attr" "cwtch.im/cwtch/model/constants" "cwtch.im/cwtch/peer" "cwtch.im/cwtch/protocol/connections" "cwtch.im/cwtch/settings" "encoding/json" "fmt" "git.openprivacy.ca/openprivacy/log" "os" "strconv" "time" ) type ImagePreviewsFunctionality struct { downloadFolder string } func (i *ImagePreviewsFunctionality) NotifySettingsUpdate(settings settings.GlobalSettings) { i.downloadFolder = settings.DownloadPath } func (i *ImagePreviewsFunctionality) EventsToRegister() []event.Type { return []event.Type{event.ProtocolEngineCreated, event.NewMessageFromPeer, event.NewMessageFromGroup, event.PeerStateChange, event.Heartbeat} } func (i *ImagePreviewsFunctionality) ExperimentsToRegister() []string { return []string{constants.FileSharingExperiment, constants.ImagePreviewsExperiment} } func (i *ImagePreviewsFunctionality) OnEvent(ev event.Event, profile peer.CwtchPeer) { if profile.IsFeatureEnabled(constants.FileSharingExperiment) && profile.IsFeatureEnabled(constants.ImagePreviewsExperiment) { switch ev.EventType { case event.NewMessageFromPeer: ci, err := profile.FetchConversationInfo(ev.Data["RemotePeer"]) if err == nil { if ci.GetPeerAC().RenderImages { i.handleImagePreviews(profile, &ev, ci.ID, ci.ID) } } case event.NewMessageFromGroup: ci, err := profile.FetchConversationInfo(ev.Data["RemotePeer"]) if err == nil { if ci.GetPeerAC().RenderImages { i.handleImagePreviews(profile, &ev, ci.ID, ci.ID) } } case event.PeerStateChange: ci, err := profile.FetchConversationInfo(ev.Data["RemotePeer"]) if err == nil { // if we have re-authenticated with this peer then request their profile image... if connections.ConnectionStateToType()[ev.Data[event.ConnectionState]] == connections.AUTHENTICATED { profile.SendScopedZonedGetValToContact(ci.ID, attr.PublicScope, attr.ProfileZone, constants.CustomProfileImageKey) } } case event.Heartbeat: conversations, err := profile.FetchConversations() if err == nil { for _, ci := range conversations { if profile.GetPeerState(ci.Handle) == connections.AUTHENTICATED { // if we have enabled file shares for this contact, then send them our profile image // NOTE: In the past, Cwtch treated "profile image" as a public file share. As such, anyone with the file key and who is able // to authenticate with the profile (i.e. non-blocked peers) can download the file (if the global profile images experiment is enabled) // To better allow for fine-grained permissions (and to support hybrid group permissions), we want to enable per-conversation file // sharing permissions. As such, profile images are now only shared with contacts with that permission enabled. // (i.e. all previous accepted contacts, new accepted contacts, and contacts who have this toggle set explictly) if ci.GetPeerAC().ShareFiles { profile.SendScopedZonedGetValToContact(ci.ID, attr.PublicScope, attr.ProfileZone, constants.CustomProfileImageKey) } } } } case event.ProtocolEngineCreated: // Now that the Peer Engine is Activated, Reshare Profile Images key, exists := profile.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.CustomProfileImageKey) if exists { serializedManifest, _ := profile.GetScopedZonedAttribute(attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", key)) // reset the share timestamp, currently file shares are hardcoded to expire after 30 days... // we reset the profile image here so that it is always available. profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.ts", key), strconv.FormatInt(time.Now().Unix(), 10)) log.Debugf("Custom Profile Image: %v %s", key, serializedManifest) f := Functionality{} f.RestartFileShare(profile, key) } } } } func (i *ImagePreviewsFunctionality) OnContactRequestValue(profile peer.CwtchPeer, conversation model.Conversation, eventID string, path attr.ScopedZonedPath) { } func (i *ImagePreviewsFunctionality) OnContactReceiveValue(profile peer.CwtchPeer, conversation model.Conversation, path attr.ScopedZonedPath, value string, exists bool) { if profile.IsFeatureEnabled(constants.FileSharingExperiment) && profile.IsFeatureEnabled(constants.ImagePreviewsExperiment) { _, zone, path := path.GetScopeZonePath() if exists && zone == attr.ProfileZone && path == constants.CustomProfileImageKey { // We only download from accepted conversations if conversation.GetPeerAC().RenderImages { fileKey := value basepath := i.downloadFolder fsf := FunctionalityGate() // We always overwrite profile image files... fp, mp := GenerateDownloadPath(basepath, fileKey, true) // If we have marked this file as complete... if value, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.complete", fileKey)); exists && value == event.True { if _, err := os.Stat(fp); err == nil { // file is marked as completed downloaded and exists... // Note: this will also resend the FileDownloaded event if successful... if fsf.VerifyOrResumeDownload(profile, conversation.ID, fileKey, constants.ImagePreviewMaxSizeInBytes) == nil { return } // Otherwise we fall through... } // Something went wrong...the file is marked as complete but either doesn't exist, or is corrupted such that we can't continue... // So mark complete as false... profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.complete", fileKey), event.False) } // If we have reached this point then we need to download the file again... log.Debugf("Downloading Profile Image %v %v %v", fp, mp, fileKey) fsf.DownloadFile(profile, conversation.ID, fp, mp, fileKey, constants.ImagePreviewMaxSizeInBytes) } } } } // handleImagePreviews checks settings and, if appropriate, auto-downloads any images func (i *ImagePreviewsFunctionality) handleImagePreviews(profile peer.CwtchPeer, ev *event.Event, conversationID, senderID int) { if profile.IsFeatureEnabled(constants.FileSharingExperiment) && profile.IsFeatureEnabled(constants.ImagePreviewsExperiment) { ci, err := profile.GetConversationInfo(senderID) if err != nil { log.Errorf("attempted to call handleImagePreviews with unknown conversation: %v", senderID) return } if !ci.GetPeerAC().ShareFiles || !ci.GetPeerAC().RenderImages { log.Infof("refusing to autodownload files from sender: %v. conversation AC does not permit image rendering", senderID) return } // Short-circuit failures // Don't auto-download images if the download path does not exist. if i.downloadFolder == "" { log.Errorf("download folder %v is not set", i.downloadFolder) return } // Don't auto-download images if the download path does not exist. if _, err := os.Stat(i.downloadFolder); os.IsNotExist(err) { log.Errorf("download folder %v does not exist", i.downloadFolder) return } // If file sharing is enabled then reshare all active files... fsf := FunctionalityGate() // Now look at the image preview experiment var cm model.MessageWrapper err = json.Unmarshal([]byte(ev.Data[event.Data]), &cm) if err == nil && cm.Overlay == model.OverlayFileSharing { log.Debugf("Received File Sharing Message") var fm OverlayMessage err = json.Unmarshal([]byte(cm.Data), &fm) if err == nil { if fm.ShouldAutoDL() { basepath := i.downloadFolder fp, mp := GenerateDownloadPath(basepath, fm.Name, false) log.Debugf("autodownloading file! %v %v %v", basepath, fp, i.downloadFolder) ev.Data["Auto"] = constants.True mID, _ := strconv.Atoi(ev.Data["Index"]) profile.UpdateMessageAttribute(conversationID, 0, mID, constants.AttrDownloaded, constants.True) fsf.DownloadFile(profile, senderID, fp, mp, fm.FileKey(), constants.ImagePreviewMaxSizeInBytes) } } } } }