diff --git a/constants/attributes.go b/constants/attributes.go new file mode 100644 index 0000000..4026122 --- /dev/null +++ b/constants/attributes.go @@ -0,0 +1,23 @@ +package constants + +const SchemaVersion = "schemaVersion" + +const Name = "name" +const LastRead = "last-read" +const Picture = "picture" +const ShowBlocked = "show-blocked" + +const ProfileTypeV1DefaultPassword = "v1-defaultPassword" +const ProfileTypeV1Password = "v1-userPassword" + +// PeerOnline stores state on if the peer believes it is online +const PeerOnline = "peer-online" + +const StateProfilePane = "state-profile-pane" +const StateSelectedConversation = "state-selected-conversation" +const StateSelectedProfileTime = "state-selected-profile-time" + +// Settings +const BlockUnknownPeersSetting = "blockunknownpeers" +const LocaleSetting = "locale" +const ZoomSetting = "zoom" \ No newline at end of file diff --git a/constants/server_manager_events.go b/constants/server_manager_events.go new file mode 100644 index 0000000..75982c3 --- /dev/null +++ b/constants/server_manager_events.go @@ -0,0 +1,26 @@ +package constants + +import "cwtch.im/cwtch/event" + +// The server manager defines its own events, most should be self-explanatory: +const ( + NewServer = event.Type("NewServer") + + // Force a UI update + ListServers = event.Type("ListServers") + + // Takes an Onion, used to toggle off/on Server availability + StartServer = event.Type("StartServer") + StopServer = event.Type("StopServer") + + // Takes an Onion and a AutoStartEnabled boolean + AutoStart = event.Type("AutoStart") + + // Get the status of a particular server (takes an Onion) + CheckServerStatus = event.Type("CheckServerStatus") + ServerStatusUpdate = event.Type("ServerStatusUpdate") +) + +const ( + AutoStartEnabled = event.Field("AutoStartEnabled") +) diff --git a/lib.go b/lib.go index 31b629b..a67953b 100644 --- a/lib.go +++ b/lib.go @@ -1,5 +1,5 @@ -package cwtch -//package main +//package cwtch +package main import "C" import ( @@ -7,8 +7,13 @@ import ( "cwtch.im/cwtch/app" "cwtch.im/cwtch/event" "cwtch.im/cwtch/peer" + "cwtch.im/cwtch/model/attr" + "cwtch.im/cwtch/app/plugins" + "encoding/json" "fmt" + "git.openprivacy.ca/flutter/libcwtch-go/constants" + "git.openprivacy.ca/flutter/libcwtch-go/utils" "encoding/base64" "git.openprivacy.ca/openprivacy/connectivity/tor" @@ -22,6 +27,7 @@ import ( var application app.Application var appBusQueue event.Queue +var profileRepaintQueue event.Queue var acnQueue event.Queue var contactEventsQueue event.Queue @@ -81,6 +87,10 @@ func StartCwtch(appDir string, torPath string) { event.NewGetValMessageFromPeer, event.PeerStateChange, } + + profileRepaintQueue = event.NewQueue() + newApp.GetPrimaryBus().Subscribe(event.NewPeer, profileRepaintQueue) + newApp.LoadProfiles("be gay do crime") newApp.LaunchPeers() application = newApp @@ -101,8 +111,8 @@ func ACNEvents() string { } } -//export c_AppBusEvent -func c_AppBusEvent() *C.char { +//export c_GetAppBusEvent +func c_GetAppBusEvent() *C.char { return C.CString(GetAppBusEvent()) } @@ -111,13 +121,62 @@ func c_AppBusEvent() *C.char { func GetAppBusEvent() string { e := appBusQueue.Next() if e.EventType == event.NewPeer { - e.Data[event.ProfileName] = "orb Quen" - e.Data[event.Path] = "profiles/044-witch.png" + //e.Data[event.ProfileName] = "orb Quen" + //e.Data[event.Path] = "profiles/044-witch.png" + onion := e.Data[event.Identity] + profile := application.GetPeer(e.Data[event.Identity]) + + if e.Data[event.Created] == event.True { + profile.SetAttribute(attr.GetPublicScope(constants.Name), profile.GetName()) + profile.SetAttribute(attr.GetPublicScope(constants.Picture), utils.ImageToString(utils.NewImage(utils.RandomProfileImage(onion), utils.TypeImageDistro))) + } + if e.Data[event.Status] != event.StorageRunning || e.Data[event.Created] == event.True { + profile.SetAttribute(attr.GetLocalScope(constants.PeerOnline), event.False) + application.AddPeerPlugin(onion, plugins.CONNECTIONRETRY) + application.AddPeerPlugin(onion, plugins.NETWORKCHECK) + } + + nick, exists := profile.GetAttribute(attr.GetPublicScope(constants.Name)) + if !exists { + nick = onion + } + + picVal, ok := profile.GetAttribute(attr.GetPublicScope(constants.Picture)) + if !ok { + picVal = utils.ImageToString(utils.NewImage(utils.RandomProfileImage(onion), utils.TypeImageDistro)) + } + pic, err := utils.StringToImage(picVal) + if err != nil { + pic = utils.NewImage(utils.RandomProfileImage(onion), utils.TypeImageDistro) + } + picPath := utils.GetPicturePath(pic) + + //tag, _ := profile.GetAttribute(app.AttributeTag) + + online, _ := profile.GetAttribute(attr.GetLocalScope(constants.PeerOnline)) + + e.Data[constants.Name] = nick + e.Data[constants.Picture] = picPath + e.Data["Online"] = online } ba, _ := json.Marshal(e) return string(ba) } +//export c_GetProfileRepaintEvent +func c_GetProfileRepaintEvent() int8 { + if GetProfileRepaintEvent() { + return 1 + } else { + return 0 + } +} + +func GetProfileRepaintEvent() bool { + <- acnQueue.OutChan() + return true +} + type Profile struct { Name string `json:"name"` Onion string `json:"onion"` @@ -157,7 +216,7 @@ func c_GetContacts(onion_ptr *C.char, onion_len C.int) *C.char { return C.CString(GetContacts(C.GoStringN(onion_ptr, onion_len))) } -func GetContacts(onion string)string { +func GetContacts(onion string) string { log.Infof("Get Contacts for %v", onion) mypeer := application.GetPeer(onion) @@ -220,10 +279,10 @@ func ContactEvents() string { } //export c_NumMessages -func c_NumMessages(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int) (n C.int) { +func c_NumMessages(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int) (n int) { profile := C.GoStringN(profile_ptr, profile_len) handle := C.GoStringN(handle_ptr, handle_len) - return C.int(NumMessages(profile, handle)) + return (NumMessages(profile, handle)) } func NumMessages(profile, handle string) (n int) { @@ -261,4 +320,4 @@ func GetMessages(profile, handle string, start, end int) string { } // Leave as is, needed by ffi -//func main() {} \ No newline at end of file +func main() {} \ No newline at end of file diff --git a/utils/imageType.go b/utils/imageType.go new file mode 100644 index 0000000..72faa1e --- /dev/null +++ b/utils/imageType.go @@ -0,0 +1,34 @@ +package utils + +import "encoding/json" + +// Image types we support +const ( + // TypeImageDistro is a reletive path to any of the distributed images in cwtch/ui in the assets folder + TypeImageDistro = "distro" + // TypeImageComposition will be an face image composed of a recipe of parts like faceType, eyeType, etc + TypeImageComposition = "composition" +) + +type image struct { + Val string + T string +} + +func NewImage(val, t string) *image { + return &image{val, t} +} + +func StringToImage(str string) (*image, error) { + var img image + err := json.Unmarshal([]byte(str), &img) + if err != nil { + return nil, err + } + return &img, nil +} + +func ImageToString(img *image) string { + bytes, _ := json.Marshal(img) + return string(bytes) +} diff --git a/utils/manager.go b/utils/manager.go new file mode 100644 index 0000000..fd9fcae --- /dev/null +++ b/utils/manager.go @@ -0,0 +1,388 @@ +package utils + +import ( + "cwtch.im/cwtch/model" + "cwtch.im/cwtch/model/attr" + "cwtch.im/cwtch/peer" + "git.openprivacy.ca/flutter/libcwtch-go/constants" + "git.openprivacy.ca/openprivacy/log" + "strings" + "time" +) + +type PeerHelper struct { + peer peer.CwtchPeer +} + +func NewPeerHelper(profile peer.CwtchPeer) *PeerHelper { + return &PeerHelper{profile} +} + +func (p *PeerHelper) IsGroup( id string) bool { + return len(id) == 32 && !p.IsServer(id) +} + +func (p *PeerHelper) IsPeer(id string) bool { + return len(id) == 56 && !p.IsServer(id) +} + +// Check if the id is associated with a contact with a KeyTypeServerOnion attribute (which indicates that this +// is a server, not a regular contact or a group +func (p *PeerHelper) IsServer(id string) bool { + _, ok := p.peer.GetContactAttribute(id, string(model.KeyTypeServerOnion)) + return ok +} + +/* +func getOrDefault(id, key string, defaultVal string) string { + var val string + var ok bool + if IsGroup(id) { + val, ok = the.Peer.GetGroupAttribute(id, key) + } else { + val, ok = the.Peer.GetContactAttribute(id, key) + } + if ok { + return val + } else { + return defaultVal + } +}*/ + +func (p *PeerHelper) GetWithSetDefault(id string, key string, defaultVal string) string { + var val string + var ok bool + if p.IsGroup(id) { + val, ok = p.peer.GetGroupAttribute(id, key) + } else { + val, ok = p.peer.GetContactAttribute(id, key) + } + if !ok { + val = defaultVal + if p.IsGroup(id) { + p.peer.SetGroupAttribute(id, key, defaultVal) + } else { + p.peer.SetContactAttribute(id, key, defaultVal) + } + } + return val +} + +func (p *PeerHelper) GetNick(id string) string { + if p.IsGroup(id) { + nick, exists := p.peer.GetGroupAttribute(id, attr.GetLocalScope(constants.Name)) + if !exists || nick == "" || nick == id { + nick, exists = p.peer.GetGroupAttribute(id, attr.GetPeerScope(constants.Name)) + if !exists { + nick = "[" + id + "]" + } + } + return nick + } else { + nick, exists := p.peer.GetContactAttribute(id, attr.GetLocalScope(constants.Name)) + if !exists || nick == "" || nick == id { + nick, exists = p.peer.GetContactAttribute(id, attr.GetPeerScope(constants.Name)) + if !exists { + nick = "[" + id + "]" + } + } + return nick + } +} + +// InitLastReadTime checks and gets the Attributable's LastRead time or sets it to now +func (p *PeerHelper) InitLastReadTime(id string) time.Time { + nowStr, _ := time.Now().MarshalText() + lastReadAttr := p.GetWithSetDefault(id, attr.GetLocalScope(constants.LastRead), string(nowStr)) + var lastRead time.Time + lastRead.UnmarshalText([]byte(lastReadAttr)) + return lastRead +} + +// GetProfilePic returns a string path to an image to display for hte given peer/group id +func (p *PeerHelper) GetProfilePic(id string) string { + if p.IsGroup(id) { + if picVal, exists := p.peer.GetGroupAttribute(id, attr.GetLocalScope(constants.Picture)); exists { + pic, err := StringToImage(picVal) + if err == nil { + return GetPicturePath(pic) + } + } + if picVal, exists := p.peer.GetGroupAttribute(id, attr.GetPeerScope(constants.Picture)); exists { + pic, err := StringToImage(picVal) + if err == nil { + return GetPicturePath(pic) + } + } + return GetPicturePath(NewImage(RandomGroupImage(id), TypeImageDistro)) + + } else { + if picVal, exists := p.peer.GetContactAttribute(id, attr.GetLocalScope(constants.Picture)); exists { + pic, err := StringToImage(picVal) + if err == nil { + return GetPicturePath(pic) + } + } + if picVal, exists := p.peer.GetContactAttribute(id, attr.GetPeerScope(constants.Picture)); exists { + pic, err := StringToImage(picVal) + if err == nil { + return GetPicturePath(pic) + } + } + return RandomProfileImage(id) + } +} + +// a lot of pics were stored full path + uri. remove all this to the relative path in images/ +// fix for storing full paths introduced 2019.12 +func profilePicRelativize(filename string) string { + parts := strings.Split(filename, "qml/images") + return parts[len(parts)-1] +} + +func GetPicturePath(pic *image) string { + switch pic.T { + case TypeImageDistro: + return profilePicRelativize(pic.Val) + default: + log.Errorf("Unhandled profile picture type of %v\n", pic.T) + return "" + } +} + +func (p *PeerHelper) updateLastReadTime(id string) { + lastRead, _ := time.Now().MarshalText() + if p.IsGroup(id) { + p.peer.SetGroupAttribute(id, attr.GetLocalScope(constants.LastRead), string(lastRead)) + } else { + p.peer.SetContactAttribute(id, attr.GetLocalScope(constants.LastRead), string(lastRead)) + } +} + +func (p *PeerHelper) CountUnread(messages []model.Message, lastRead time.Time) int { + count := 0 + for i := len(messages) - 1; i >= 0; i-- { + if messages[i].Timestamp.After(lastRead) || messages[i].Timestamp.Equal(lastRead) { + count++ + } else { + break + } + } + return count +} +/* +// AddProfile adds a new profile to the UI +func AddProfile(gcd *GrandCentralDispatcher, handle string) { + p := the.CwtchApp.GetPeer(handle) + if p != nil { + nick, exists := p.GetAttribute(attr.GetPublicScope(constants.Name)) + if !exists { + nick = handle + } + + picVal, ok := p.GetAttribute(attr.GetPublicScope(constants.Picture)) + if !ok { + picVal = ImageToString(NewImage(RandomProfileImage(handle), TypeImageDistro)) + } + pic, err := StringToImage(picVal) + if err != nil { + pic = NewImage(RandomProfileImage(handle), TypeImageDistro) + } + picPath := getPicturePath(pic) + + tag, _ := p.GetAttribute(app.AttributeTag) + + online, _ := p.GetAttribute(attr.GetLocalScope(constants.PeerOnline)) + + log.Debugf("AddProfile %v %v %v %v %v\n", handle, nick, picPath, tag, online) + gcd.AddProfile(handle, nick, picPath, tag, online == event.True) + } +}*/ +/* +type manager struct { + gcd *GrandCentralDispatcher + profile string +} + +// Manager is a middleware helper for entities like peer event listeners wishing to trigger ui changes (via the gcd) +// each manager is for one profile/peer +// manager takes minimal arguments and builds the full struct of data (usually pulled from a cwtch peer) required to call the GCD to perform the ui action +// manager also performs call filtering based on UI state: users of manager can safely always call it on events and not have to worry about weather the relevant ui is active +// ie: you can always safely call AddMessage even if in the ui a different profile is selected. manager will check with gcd, and if the correct conditions are not met, it will not call on gcd to update the ui incorrectly +type Manager interface { + Acknowledge(handle, mID string) + AddContact(Handle string) + AddSendMessageError(peer string, signature string, err string) + AddMessage(handle string, from string, message string, fromMe bool, messageID string, timestamp time.Time, Acknowledged bool) + + ReloadProfiles() + + UpdateContactDisplayName(handle string) + UpdateContactPicture(handle string) + UpdateContactStatus(handle string, status int, loading bool) + UpdateContactAttribute(handle, key, value string) + + ChangePasswordResponse(error bool) + + AboutToAddMessage() + MessageJustAdded() + StoreAndNotify(peer.CwtchPeer, string, string, time.Time, string) + + UpdateNetworkStatus(online bool) +} + +// NewManager returns a new Manager interface for a profile to the gcd +func NewManager(profile string, gcd *GrandCentralDispatcher) Manager { + return &manager{gcd: gcd, profile: profile} +} + +// Acknowledge acknowledges the given message id in the UI +func (this *manager) Acknowledge(handle, mID string) { + this.gcd.DoIfProfile(this.profile, func() { + this.gcd.DoIfConversation(handle, func() { + this.gcd.PeerAckAlert(mID) + }) + }) +} + +func getLastMessageTime(tl *model.Timeline) int { + if len(tl.Messages) == 0 { + return 0 + } + + return int(tl.Messages[len(tl.Messages)-1].Timestamp.Unix()) +} + +// AddContact adds a new contact to the ui for this manager's profile +func (this *manager) AddContact(handle string) { + this.gcd.DoIfProfile(this.profile, func() { + + if IsGroup(handle) { + group := the.Peer.GetGroup(handle) + if group != nil { + lastRead := InitLastReadTime(group.GroupID) + unread := CountUnread(group.Timeline.GetMessages(), lastRead) + picture := GetProfilePic(handle) + + this.gcd.AddContact(handle, GetNick(handle), picture, unread, int(connections.ConnectionStateToType[group.State]), string(model.AuthApproved), false, getLastMessageTime(&group.Timeline)) + } + return + } else if !IsPeer(handle) { + log.Errorf("sorry, unable to handle AddContact(%v)", handle) + debug.PrintStack() + return + } + + contact := the.Peer.GetContact(handle) + if contact != nil { + lastRead := InitLastReadTime(contact.Onion) + unread := CountUnread(contact.Timeline.GetMessages(), lastRead) + picture := GetProfilePic(handle) + + this.gcd.AddContact(handle, GetNick(handle), picture, unread, int(connections.ConnectionStateToType[contact.State]), string(contact.Authorization), false, getLastMessageTime(&contact.Timeline)) + } + }) +} + +// AddSendMessageError adds an error not and icon to a message in a conversation in the ui for the message identified by the peer/sig combo +func (this *manager) AddSendMessageError(peer string, signature string, err string) { + this.gcd.DoIfProfile(this.profile, func() { + this.gcd.DoIfConversation(peer, func() { + log.Debugf("Received Error Sending Message: %v", err) + // FIXME: Sometimes, for the first Peer message we send our error beats our message to the UI + time.Sleep(time.Second * 1) + this.gcd.GroupSendError(signature, err) + }) + }) +} + +func (this *manager) AboutToAddMessage() { + this.gcd.TimelineInterface.AddMessage(this.gcd.TimelineInterface.num()) +} + +func (this *manager) MessageJustAdded() { + this.gcd.TimelineInterface.RequestEIR() +} + +func (this *manager) StoreAndNotify(pere peer.CwtchPeer, onion string, messageTxt string, sent time.Time, profileOnion string) { + + // Send a New Message from Peer Notification + this.gcd.AndroidCwtchActivity.SetChannel(onion) + this.gcd.AndroidCwtchActivity.NotificationChanged("New Message from Peer") + + this.gcd.DoIfProfileElse(this.profile, func() { + this.gcd.DoIfConversationElse(onion, func() { + this.gcd.TimelineInterface.AddMessage(this.gcd.TimelineInterface.num()) + pere.StoreMessage(onion, messageTxt, sent) + this.gcd.TimelineInterface.RequestEIR() + updateLastReadTime(onion) + }, func() { + pere.StoreMessage(onion, messageTxt, sent) + }) + this.gcd.IncContactUnreadCount(onion) + }, func() { + the.CwtchApp.GetPeer(profileOnion).StoreMessage(onion, messageTxt, sent) + }) + this.gcd.Notify(onion) +} + +// AddMessage adds a message to the message pane for the supplied conversation if it is active +func (this *manager) AddMessage(handle string, from string, message string, fromMe bool, messageID string, timestamp time.Time, Acknowledged bool) { + this.gcd.DoIfProfile(this.profile, func() { + this.gcd.DoIfConversation(handle, func() { + updateLastReadTime(handle) + // If the message is not from the user then add it, otherwise, just acknowledge. + if !fromMe || !Acknowledged { + this.gcd.TimelineInterface.AddMessage(this.gcd.TimelineInterface.num() - 1) + this.gcd.TimelineInterface.RequestEIR() + } else { + this.gcd.Acknowledged(messageID) + } + }) + this.gcd.IncContactUnreadCount(handle) + }) + if !fromMe { + this.gcd.Notify(handle) + } +} + +func (this *manager) ReloadProfiles() { + this.gcd.reloadProfileList() +} + +// UpdateContactDisplayName updates a contact's display name in the contact list and conversations +func (this *manager) UpdateContactDisplayName(handle string) { + this.gcd.DoIfProfile(this.profile, func() { + this.gcd.UpdateContactDisplayName(handle, GetNick(handle)) + }) +} + +// UpdateContactPicture updates a contact's picture in the contact list and conversations +func (this *manager) UpdateContactPicture(handle string) { + this.gcd.DoIfProfile(this.profile, func() { + this.gcd.UpdateContactPicture(handle, GetProfilePic(handle)) + }) +} + +// UpdateContactStatus updates a contact's status in the ui +func (this *manager) UpdateContactStatus(handle string, status int, loading bool) { + this.gcd.DoIfProfile(this.profile, func() { + this.gcd.UpdateContactStatus(handle, status, loading) + }) +} + +// UpdateContactAttribute update's a contacts attribute in the ui +func (this *manager) UpdateContactAttribute(handle, key, value string) { + this.gcd.DoIfProfile(this.profile, func() { + this.gcd.UpdateContactAttribute(handle, key, value) + }) +} + +func (this *manager) ChangePasswordResponse(error bool) { + this.gcd.ChangePasswordResponse(error) +} + +func (this *manager) UpdateNetworkStatus(online bool) { + this.gcd.UpdateProfileNetworkStatus(this.profile, online) +} +*/ \ No newline at end of file diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..8561ad0 --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,29 @@ +package utils + +import ( + "encoding/base32" + "encoding/hex" + "git.openprivacy.ca/openprivacy/log" + "strings" +) + +// temporary until we do real picture selection +func RandomProfileImage(onion string) string { + choices := []string{"001-centaur", "002-kraken", "003-dinosaur", "004-tree-1", "005-hand", "006-echidna", "007-robot", "008-mushroom", "009-harpy", "010-phoenix", "011-dragon-1", "012-devil", "013-troll", "014-alien", "015-minotaur", "016-madre-monte", "017-satyr", "018-karakasakozou", "019-pirate", "020-werewolf", "021-scarecrow", "022-valkyrie", "023-curupira", "024-loch-ness-monster", "025-tree", "026-cerberus", "027-gryphon", "028-mermaid", "029-vampire", "030-goblin", "031-yeti", "032-leprechaun", "033-medusa", "034-chimera", "035-elf", "036-hydra", "037-cyclops", "038-pegasus", "039-narwhal", "040-woodcutter", "041-zombie", "042-dragon", "043-frankenstein", "044-witch", "045-fairy", "046-genie", "047-pinocchio", "048-ghost", "049-wizard", "050-unicorn"} + barr, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion)) + if err != nil || len(barr) != 35 { + log.Errorf("error: %v %v %v\n", onion, err, barr) + return "extra/openprivacy.png" + } + return "profiles/" + choices[int(barr[33])%len(choices)] + ".png" +} + +func RandomGroupImage(handle string) string { + choices := []string{"001-borobudur", "002-opera-house", "003-burj-al-arab", "004-chrysler", "005-acropolis", "006-empire-state-building", "007-temple", "008-indonesia-1", "009-new-zealand", "010-notre-dame", "011-space-needle", "012-seoul", "013-mosque", "014-milan", "015-statue", "016-pyramid", "017-cologne", "018-brandenburg-gate", "019-berlin-cathedral", "020-hungarian-parliament", "021-buckingham", "022-thailand", "023-independence", "024-angkor-wat", "025-vaticano", "026-christ-the-redeemer", "027-colosseum", "028-golden-gate-bridge", "029-sphinx", "030-statue-of-liberty", "031-cradle-of-humankind", "032-istanbul", "033-london-eye", "034-sagrada-familia", "035-tower-bridge", "036-burj-khalifa", "037-washington", "038-big-ben", "039-stonehenge", "040-white-house", "041-ahu-tongariki", "042-capitol", "043-eiffel-tower", "044-church-of-the-savior-on-spilled-blood", "045-arc-de-triomphe", "046-windmill", "047-louvre", "048-torii-gate", "049-petronas", "050-matsumoto-castle", "051-fuji", "052-temple-of-heaven", "053-pagoda", "054-chichen-itza", "055-forbidden-city", "056-merlion", "057-great-wall-of-china", "058-taj-mahal", "059-pisa", "060-indonesia"} + barr, err := hex.DecodeString(handle) + if err != nil || len(barr) == 0 { + log.Errorf("error: %v %v %v\n", handle, err, barr) + return "extra/openprivacy.png" + } + return "servers/" + choices[int(barr[0])%len(choices)] + ".png" +}