This repository has been archived on 2023-06-16. You can view files and clone it, but cannot push or open issues or pull requests.
libcwtch-go/utils/eventHandler.go

464 lines
18 KiB
Go
Raw Normal View History

2021-06-24 22:30:46 +00:00
package utils
import (
"cwtch.im/cwtch/app"
"cwtch.im/cwtch/app/plugins"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
2021-06-24 22:30:46 +00:00
"cwtch.im/cwtch/protocol/connections"
"encoding/json"
constants2 "git.openprivacy.ca/cwtch.im/libcwtch-go/constants"
"git.openprivacy.ca/cwtch.im/libcwtch-go/features/groups"
2021-10-29 23:14:08 +00:00
"git.openprivacy.ca/cwtch.im/libcwtch-go/features/servers"
2021-06-24 22:30:46 +00:00
"git.openprivacy.ca/openprivacy/log"
"strconv"
2021-11-17 20:33:51 +00:00
"time"
2021-06-24 22:30:46 +00:00
)
import "cwtch.im/cwtch/event"
type EventProfileEnvelope struct {
Event event.Event
Profile string
}
type EventHandler struct {
app app.Application
appBusQueue event.Queue
profileEvents chan EventProfileEnvelope
}
func NewEventHandler() *EventHandler {
eh := &EventHandler{app: nil, appBusQueue: event.NewQueue(), profileEvents: make(chan EventProfileEnvelope)}
return eh
}
// PublishAppEvent is a way for libCwtch-go to publish an event for consumption by a UI before a Cwtch app has been initialized
// Main use: to signal an error before a cwtch app could be created
func (eh *EventHandler) PublishAppEvent(event event.Event) {
eh.appBusQueue.Publish(event)
}
func (eh *EventHandler) HandleApp(application app.Application) {
eh.app = application
application.GetPrimaryBus().Subscribe(event.NewPeer, eh.appBusQueue)
application.GetPrimaryBus().Subscribe(event.PeerError, eh.appBusQueue)
application.GetPrimaryBus().Subscribe(event.PeerDeleted, eh.appBusQueue)
application.GetPrimaryBus().Subscribe(event.Shutdown, eh.appBusQueue)
application.GetPrimaryBus().Subscribe(event.AppError, eh.appBusQueue)
application.GetPrimaryBus().Subscribe(event.ACNStatus, eh.appBusQueue)
application.GetPrimaryBus().Subscribe(event.ReloadDone, eh.appBusQueue)
application.GetPrimaryBus().Subscribe(event.ACNVersion, eh.appBusQueue)
application.GetPrimaryBus().Subscribe(UpdateGlobalSettings, eh.appBusQueue)
application.GetPrimaryBus().Subscribe(CwtchStarted, eh.appBusQueue)
2021-10-29 23:14:08 +00:00
application.GetPrimaryBus().Subscribe(servers.NewServer, eh.appBusQueue)
application.GetPrimaryBus().Subscribe(servers.ServerIntentUpdate, eh.appBusQueue)
2021-11-04 01:19:30 +00:00
application.GetPrimaryBus().Subscribe(servers.ServerDeleted, eh.appBusQueue)
2021-06-24 22:30:46 +00:00
}
func (eh *EventHandler) GetNextEvent() string {
appChan := eh.appBusQueue.OutChan()
select {
case e := <-appChan:
return eh.handleAppBusEvent(&e)
2021-11-17 20:33:51 +00:00
default:
select {
case e := <-appChan:
return eh.handleAppBusEvent(&e)
case ev := <-eh.profileEvents:
return eh.handleProfileEvent(&ev)
}
2021-06-24 22:30:46 +00:00
}
}
// handleAppBusEvent enriches AppBus events so they are usable with out further data fetches
func (eh *EventHandler) handleAppBusEvent(e *event.Event) string {
if eh.app != nil {
switch e.EventType {
case event.ACNStatus:
if e.Data[event.Progress] == "100" {
for _, onion := range eh.app.ListProfiles() {
2021-06-24 22:30:46 +00:00
// launch a listen thread (internally this does a check that the protocol engine is not listening)
// and as such is safe to call.
eh.app.GetPeer(onion).Listen()
}
}
case event.NewPeer:
onion := e.Data[event.Identity]
profile := eh.app.GetPeer(e.Data[event.Identity])
log.Debug("New Peer Event: %v", e)
if e.Data["Reload"] != event.True {
eh.startHandlingPeer(onion)
}
// CwtchPeer will always set this now...
tag, _ := profile.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Tag)
e.Data[constants.Tag] = tag
2021-06-24 22:30:46 +00:00
if e.Data[event.Created] == event.True {
profile.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants2.Picture, ImageToString(NewImage(RandomProfileImage(onion), TypeImageDistro)))
2021-06-24 22:30:46 +00:00
}
if e.Data[event.Status] != event.StorageRunning || e.Data[event.Created] == event.True {
profile.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants2.PeerOnline, event.False)
2021-06-24 22:30:46 +00:00
eh.app.AddPeerPlugin(onion, plugins.CONNECTIONRETRY)
eh.app.AddPeerPlugin(onion, plugins.NETWORKCHECK)
// If the user has chosen to block unknown profiles
// then explicitly configure the protocol engine to do so..
if ReadGlobalSettings().BlockUnknownConnections {
profile.BlockUnknownConnections()
} else {
// For completeness
profile.AllowUnknownConnections()
}
// Start up the Profile
profile.Listen()
profile.StartPeersConnections()
if _, err := groups.ExperimentGate(ReadGlobalSettings().Experiments); err == nil {
profile.StartServerConnections()
}
}
online, _ := profile.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants2.PeerOnline)
2021-10-15 20:48:05 +00:00
// Name always exists
e.Data[constants.Name], _ = profile.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
2021-11-26 23:07:20 +00:00
// Resolve the profile image of the profile.
2021-11-17 20:33:51 +00:00
e.Data[constants2.Picture] = RandomProfileImage(onion)
2021-06-24 22:30:46 +00:00
e.Data["Online"] = online
2021-11-26 23:07:20 +00:00
// Construct our conversations and our srever lists
2021-06-24 22:30:46 +00:00
var contacts []Contact
var servers []groups.Server
2021-11-17 20:33:51 +00:00
conversations, err := profile.FetchConversations()
2021-11-26 23:07:20 +00:00
if err == nil {
// We have conversations attached to this profile...
for _, conversationInfo := range conversations {
// Only compile the server info if we have enabled the experiment...
// Note that this means that this info can become stale if when first loaded the experiment
// has been disabled and then is later re-enabled. As such we need to ensure that this list is
// re-fetched when the group experiment is enabled via a dedicated ListServerInfo event...
if conversationInfo.IsServer() {
groupHandler, err := groups.ExperimentGate(ReadGlobalSettings().Experiments)
if err == nil {
servers = append(servers, groupHandler.GetServerInfo(conversationInfo.Handle, profile))
}
continue
2021-06-24 22:30:46 +00:00
}
2021-11-26 23:07:20 +00:00
// Prefer local override to public name...
name, exists := conversationInfo.GetAttribute(attr.LocalScope, attr.ProfileZone, constants.Name)
2021-11-18 23:44:21 +00:00
if !exists {
2021-11-26 23:07:20 +00:00
name, exists = conversationInfo.GetAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
if !exists {
name = conversationInfo.Handle
}
2021-11-18 23:44:21 +00:00
}
2021-06-24 22:30:46 +00:00
2021-11-26 23:07:20 +00:00
// Resolve the profile image of the contact
var cpicPath string
if conversationInfo.IsGroup() {
cpicPath = RandomGroupImage(conversationInfo.Handle)
} else {
cpicPath = RandomProfileImage(conversationInfo.Handle)
}
2021-06-24 22:30:46 +00:00
2021-11-26 23:07:20 +00:00
// Resolve Save History Setting
saveHistory, set := conversationInfo.GetAttribute(attr.LocalScope, attr.ProfileZone, event.SaveHistoryKey)
if !set {
saveHistory = event.DeleteHistoryDefault
}
2021-11-17 20:33:51 +00:00
2021-11-26 23:07:20 +00:00
// Resolve Archived Setting
isArchived, set := conversationInfo.GetAttribute(attr.LocalScope, attr.ProfileZone, constants2.Archived)
if !set {
isArchived = event.False
}
2021-11-17 20:33:51 +00:00
2021-11-26 23:07:20 +00:00
// Resolve Peer State (should probably be DISCONNECTED)
state := profile.GetPeerState(conversationInfo.Handle)
if !set {
state = connections.DISCONNECTED
}
2021-11-17 20:33:51 +00:00
2021-11-26 23:07:20 +00:00
// Resolve Conversation Auth State
// TODO: Align this with ACLs
authorization := model.AuthUnknown
if conversationInfo.Accepted {
authorization = model.AuthApproved
}
2021-11-17 20:33:51 +00:00
2021-11-26 23:07:20 +00:00
// If ACL has blocked conversation then hide them...
if acl, exists := conversationInfo.ACL[conversationInfo.Handle]; exists && acl.Blocked {
authorization = model.AuthBlocked
}
// Check if we are a server...
groupServer, _ := conversationInfo.GetAttribute(attr.LocalScope, attr.LegacyGroupZone, constants.GroupServer)
// Fetch the message count, and the time of the most recent message
count, err := profile.GetChannelMessageCount(conversationInfo.ID, 0)
if err != nil {
log.Errorf("error fetching channel message count %v %v", conversationInfo.ID, err)
}
lastMessage, _ := profile.GetMostRecentMessages(conversationInfo.ID, 0, 0, 1)
contacts = append(contacts, Contact{
Name: name,
Identifier: conversationInfo.ID,
Onion: conversationInfo.Handle,
Status: connections.ConnectionStateName[state],
Picture: cpicPath,
Authorization: string(authorization),
SaveHistory: saveHistory,
Messages: count,
Unread: 0,
LastMessage: strconv.Itoa(getLastMessageTime(lastMessage)),
IsGroup: conversationInfo.IsGroup(),
GroupServer: groupServer,
IsArchived: isArchived == event.True,
})
}
2021-06-24 22:30:46 +00:00
}
bytes, _ := json.Marshal(contacts)
e.Data["ContactsJson"] = string(bytes)
// Marshal the server list into the new peer event...
serversListBytes, _ := json.Marshal(servers)
e.Data[groups.ServerList] = string(serversListBytes)
log.Debugf("contactsJson %v", e.Data["ContactsJson"])
}
}
json, _ := json.Marshal(e)
return string(json)
}
// handleProfileEvent enriches Profile events so they are usable with out further data fetches
func (eh *EventHandler) handleProfileEvent(ev *EventProfileEnvelope) string {
if eh.app == nil {
log.Errorf("eh.app == nil in handleProfileEvent... this shouldnt happen?")
} else {
2021-11-17 20:33:51 +00:00
profile := eh.app.GetPeer(ev.Profile)
2021-06-24 22:30:46 +00:00
log.Debugf("New Profile Event to Handle: %v", ev)
switch ev.Event.EventType {
case event.NewMessageFromPeer: //event.TimestampReceived, event.RemotePeer, event.Data
// only needs contact nickname and picture, for displaying on popup notifications
2021-11-17 20:33:51 +00:00
ci, err := profile.FetchConversationInfo(ev.Event.Data["RemotePeer"])
if ci != nil && err == nil {
ev.Event.Data[event.ConversationID] = strconv.Itoa(ci.ID)
profile.SetConversationAttribute(ci.ID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants2.Archived)), event.False)
2021-11-17 22:34:35 +00:00
} else {
// TODO This Conversation May Not Exist Yet...But we are not in charge of creating it...
2021-11-18 23:44:21 +00:00
log.Errorf("todo wait for contact to be added before processing this event...")
return ""
2021-11-17 20:33:51 +00:00
}
2021-11-26 23:07:20 +00:00
var exists bool
ev.Event.Data["Nick"], exists = ci.GetAttribute(attr.LocalScope, attr.ProfileZone, constants.Name)
if !exists {
ev.Event.Data["Nick"], exists = ci.GetAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
if !exists {
ev.Event.Data["Nick"] = ev.Event.Data["RemotePeer"]
}
2021-11-26 23:07:20 +00:00
}
2021-11-17 20:33:51 +00:00
ev.Event.Data["Picture"] = RandomProfileImage(ev.Event.Data["RemotePeer"])
2021-06-24 22:30:46 +00:00
case event.NewMessageFromGroup:
// only needs contact nickname and picture, for displaying on popup notifications
2021-11-17 20:33:51 +00:00
ci, err := profile.FetchConversationInfo(ev.Event.Data["RemotePeer"])
if ci != nil && err == nil {
2021-11-26 23:07:20 +00:00
var exists bool
ev.Event.Data["Nick"], exists = ci.GetAttribute(attr.LocalScope, attr.ProfileZone, constants.Name)
if !exists {
ev.Event.Data["Nick"], exists = ci.GetAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
if !exists {
ev.Event.Data["Nick"] = ev.Event.Data["RemotePeer"]
}
2021-11-26 23:07:20 +00:00
}
2021-11-17 20:33:51 +00:00
}
ev.Event.Data["Picture"] = RandomProfileImage(ev.Event.Data[event.RemotePeer])
2021-11-18 23:44:21 +00:00
conversationID, _ := strconv.Atoi(ev.Event.Data[event.ConversationID])
2021-11-17 20:33:51 +00:00
profile.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants2.Archived)), event.False)
2021-06-24 22:30:46 +00:00
case event.PeerAcknowledgement:
2021-11-17 20:33:51 +00:00
ci, err := profile.FetchConversationInfo(ev.Event.Data["RemotePeer"])
if ci != nil && err == nil {
ev.Event.Data[event.ConversationID] = strconv.Itoa(ci.ID)
}
2021-11-17 22:34:35 +00:00
case event.ContactCreated:
2021-11-18 23:44:21 +00:00
conversationID, _ := strconv.Atoi(ev.Event.Data[event.ConversationID])
2021-11-17 22:34:35 +00:00
count, err := profile.GetChannelMessageCount(conversationID, 0)
2021-06-24 22:30:46 +00:00
if err != nil {
2021-11-17 22:34:35 +00:00
log.Errorf("error fetching channel message count %v %v", conversationID, err)
2021-06-24 22:30:46 +00:00
}
2021-11-17 22:34:35 +00:00
conversationInfo, err := profile.GetConversationInfo(conversationID)
if err != nil {
log.Errorf("error fetching conversation info for %v %v", conversationID, err)
}
// Resolve Conversation Auth State
// TODO: Align this with ACLs
authorization := model.AuthUnknown
if conversationInfo.Accepted {
authorization = model.AuthApproved
}
// If ACL has blocked conversation then hide them...
if acl, exists := conversationInfo.ACL[conversationInfo.Handle]; exists && acl.Blocked {
authorization = model.AuthBlocked
}
2021-11-18 23:44:21 +00:00
lastMessage, _ := profile.GetMostRecentMessages(conversationID, 0, 0, 1)
ev.Event.Data["unread"] = strconv.Itoa(count) // if this is a new contact with messages attached then by-definition these are unread...
ev.Event.Data["picture"] = RandomProfileImage(conversationInfo.Handle)
2021-11-17 22:34:35 +00:00
ev.Event.Data["numMessages"] = strconv.Itoa(count)
ev.Event.Data["nick"] = conversationInfo.Handle
ev.Event.Data["status"] = connections.ConnectionStateName[profile.GetPeerState(conversationInfo.Handle)]
ev.Event.Data["authorization"] = string(authorization)
2021-11-17 22:34:35 +00:00
ev.Event.Data["loading"] = "false"
ev.Event.Data["lastMsgTime"] = strconv.Itoa(getLastMessageTime(lastMessage))
2021-06-24 22:30:46 +00:00
case event.GroupCreated:
// This event should only happen after we have validated the invite, as such the error
// condition *should* never happen.
2021-11-17 20:33:51 +00:00
groupPic := RandomGroupImage(ev.Event.Data[event.GroupID])
2021-06-24 22:30:46 +00:00
ev.Event.Data["PicturePath"] = groupPic
case event.NewGroup:
// This event should only happen after we have validated the invite, as such the error
// condition *should* never happen.
serializedInvite := ev.Event.Data[event.GroupInvite]
if invite, err := model.ValidateInvite(serializedInvite); err == nil {
2021-11-17 20:33:51 +00:00
groupPic := RandomGroupImage(invite.GroupID)
2021-06-24 22:30:46 +00:00
ev.Event.Data["PicturePath"] = groupPic
} else {
log.Errorf("received a new group event which contained an invalid invite %v. this should never happen and likely means there is a bug in cwtch. Please file a ticket @ https://git.openprivcy.ca/cwtch.im/cwtch", err)
return ""
}
case event.PeerStateChange:
cxnState := connections.ConnectionStateToType()[ev.Event.Data[event.ConnectionState]]
2021-11-18 23:44:21 +00:00
contact, _ := profile.FetchConversationInfo(ev.Event.Data[event.RemotePeer])
2021-06-24 22:30:46 +00:00
2021-11-26 22:26:26 +00:00
if ev.Event.Data[event.RemotePeer] == profile.GetOnion() {
return "" // suppress events from our own profile...
}
// We do not know who this is...don't send any event until we see a message from them
// (at that point the conversation will have been created...)
2021-06-24 22:30:46 +00:00
if cxnState == connections.AUTHENTICATED && contact == nil {
return ""
}
if contact != nil {
// No enrichment needed
//uiManager.UpdateContactStatus(contact.Onion, int(cxnState), false)
if cxnState == connections.AUTHENTICATED {
// if known and authed, get vars
2021-11-17 22:34:35 +00:00
profile.SendScopedZonedGetValToContact(contact.ID, attr.PublicScope, attr.ProfileZone, constants.Name)
profile.SendScopedZonedGetValToContact(contact.ID, attr.PublicScope, attr.ProfileZone, constants2.Picture)
2021-06-24 22:30:46 +00:00
}
}
case event.NewRetValMessageFromPeer:
// auto handled event means the setting is already done, we're just deciding if we need to tell the UI
2021-11-18 23:44:21 +00:00
conversationID, _ := strconv.Atoi(ev.Event.Data[event.ConversationID])
2021-06-24 22:30:46 +00:00
scope := ev.Event.Data[event.Scope]
path := ev.Event.Data[event.Path]
exists, _ := strconv.ParseBool(ev.Event.Data[event.Exists])
2021-10-15 17:59:42 +00:00
if exists && attr.IntoScope(scope) == attr.PublicScope {
2021-11-18 23:44:21 +00:00
zone, path := attr.ParseZone(path)
2021-11-17 20:33:51 +00:00
if _, err := profile.GetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(zone.ConstructZonedPath(path))); err != nil {
2021-10-15 17:59:42 +00:00
// we have a locally set override, don't pass this remote set public scope update to UI
2021-06-24 22:30:46 +00:00
return ""
}
}
}
}
json, _ := json.Marshal(unwrap(ev))
return string(json)
}
func unwrap(original *EventProfileEnvelope) *event.Event {
unwrapped := &original.Event
unwrapped.Data["ProfileOnion"] = original.Profile
return unwrapped
}
func (eh *EventHandler) startHandlingPeer(onion string) {
eventBus := eh.app.GetEventBus(onion)
q := event.NewQueue()
2021-11-26 22:26:26 +00:00
// eventBus.Subscribe(event.NetworkStatus, q)
2021-06-24 22:30:46 +00:00
eventBus.Subscribe(event.NewMessageFromPeer, q)
2021-11-26 22:26:26 +00:00
eventBus.Subscribe(event.UpdatedProfileAttribute, q)
2021-06-24 22:30:46 +00:00
eventBus.Subscribe(event.PeerAcknowledgement, q)
eventBus.Subscribe(event.DeleteContact, q)
eventBus.Subscribe(event.AppError, q)
eventBus.Subscribe(event.IndexedAcknowledgement, q)
eventBus.Subscribe(event.IndexedFailure, q)
eventBus.Subscribe(event.ContactCreated, q)
2021-06-24 22:30:46 +00:00
eventBus.Subscribe(event.NewMessageFromGroup, q)
2021-06-29 23:38:12 +00:00
eventBus.Subscribe(event.MessageCounterResync, q)
2021-06-24 22:30:46 +00:00
eventBus.Subscribe(event.GroupCreated, q)
eventBus.Subscribe(event.NewGroup, q)
eventBus.Subscribe(event.AcceptGroupInvite, q)
2021-08-27 20:56:32 +00:00
eventBus.Subscribe(event.SetPeerAttribute, q)
2021-06-24 22:30:46 +00:00
eventBus.Subscribe(event.SetGroupAttribute, q)
eventBus.Subscribe(event.DeleteGroup, q)
eventBus.Subscribe(event.SendMessageToGroupError, q)
eventBus.Subscribe(event.SendMessageToPeerError, q)
eventBus.Subscribe(event.ServerStateChange, q)
eventBus.Subscribe(event.PeerStateChange, q)
eventBus.Subscribe(event.ChangePasswordSuccess, q)
eventBus.Subscribe(event.ChangePasswordError, q)
eventBus.Subscribe(event.NewRetValMessageFromPeer, q)
eventBus.Subscribe(event.SetAttribute, q)
eventBus.Subscribe(event.ShareManifest, q)
eventBus.Subscribe(event.ManifestSizeReceived, q)
eventBus.Subscribe(event.ManifestError, q)
eventBus.Subscribe(event.ManifestReceived, q)
eventBus.Subscribe(event.ManifestSaved, q)
eventBus.Subscribe(event.FileDownloadProgressUpdate, q)
eventBus.Subscribe(event.FileDownloaded, q)
2021-06-24 22:30:46 +00:00
go eh.forwardProfileMessages(onion, q)
}
func (eh *EventHandler) forwardProfileMessages(onion string, q event.Queue) {
2021-06-25 05:16:47 +00:00
log.Infof("Launching Forwarding Goroutine")
2021-06-24 22:30:46 +00:00
// TODO: graceful shutdown, via an injected event of special QUIT type exiting loop/go routine
for {
e := q.Next()
ev := EventProfileEnvelope{Event: e, Profile: onion}
eh.profileEvents <- ev
if ev.Event.EventType == event.Shutdown {
return
}
}
}
func (eh *EventHandler) Push(newEvent event.Event) {
eh.appBusQueue.Publish(newEvent)
}
2021-11-17 20:33:51 +00:00
func getLastMessageTime(conversationMessages []model.ConversationMessage) int {
if len(conversationMessages) == 0 {
return 0
}
time, err := time.Parse(time.RFC3339Nano, conversationMessages[0].Attr[constants.AttrSentTimestamp])
if err != nil {
return 0
}
return int(time.Unix())
2021-11-18 23:44:21 +00:00
}