2023-02-21 20:31:49 +00:00
package utils
import (
"cwtch.im/cwtch/app"
"cwtch.im/cwtch/app/plugins"
2023-07-25 21:45:10 +00:00
"cwtch.im/cwtch/functionality/servers"
2023-02-21 20:31:49 +00:00
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/protocol/connections"
2023-07-25 21:45:10 +00:00
"cwtch.im/cwtch/settings"
"encoding/json"
"fmt"
2023-02-21 20:31:49 +00:00
constants2 "git.openprivacy.ca/cwtch.im/cwtch-autobindings/constants"
2023-07-25 21:45:10 +00:00
"git.openprivacy.ca/cwtch.im/cwtch-autobindings/experiments/server_hosting"
2023-02-21 20:31:49 +00:00
"git.openprivacy.ca/openprivacy/log"
2023-07-25 21:45:10 +00:00
"os"
"strconv"
2023-02-21 20:31:49 +00:00
"time"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/functionality/filesharing"
)
type EventProfileEnvelope struct {
Event event . Event
Profile string
}
type EventHandler struct {
app app . Application
appBusQueue event . Queue
profileEvents chan EventProfileEnvelope
2023-02-28 17:56:09 +00:00
modules [ ] EventHandlerInterface
}
type EventHandlerInterface interface {
OnACNStatusEvent ( appl app . Application , e * event . Event )
2023-02-21 20:31:49 +00:00
}
2023-04-17 17:48:58 +00:00
const ReloadEvent = event . Field ( "Reload" )
2023-02-21 20:31:49 +00:00
// We should be reading from profile events pretty quickly, but we make this buffer fairly large...
const profileEventsBufferSize = 512
func NewEventHandler ( ) * EventHandler {
2023-02-28 17:56:09 +00:00
eh := & EventHandler { app : nil , appBusQueue : event . NewQueue ( ) , profileEvents : make ( chan EventProfileEnvelope , profileEventsBufferSize ) , modules : nil }
2023-02-21 20:31:49 +00:00
return eh
}
2023-02-28 17:56:09 +00:00
func ( eh * EventHandler ) AddModule ( ehi EventHandlerInterface ) {
eh . modules = append ( eh . modules , ehi )
}
2023-02-21 20:31:49 +00:00
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 . ACNVersion , eh . appBusQueue )
2023-03-13 20:12:50 +00:00
application . GetPrimaryBus ( ) . Subscribe ( settings . UpdateGlobalSettings , eh . appBusQueue )
application . GetPrimaryBus ( ) . Subscribe ( settings . CwtchStarted , eh . appBusQueue )
2023-07-25 21:45:10 +00:00
application . GetPrimaryBus ( ) . Subscribe ( server_hosting . NewServer , eh . appBusQueue )
application . GetPrimaryBus ( ) . Subscribe ( server_hosting . ServerIntentUpdate , eh . appBusQueue )
application . GetPrimaryBus ( ) . Subscribe ( server_hosting . ServerDeleted , eh . appBusQueue )
application . GetPrimaryBus ( ) . Subscribe ( server_hosting . ServerStatsUpdate , eh . appBusQueue )
2023-02-21 20:31:49 +00:00
application . GetPrimaryBus ( ) . Subscribe ( event . StartingStorageMiragtion , eh . appBusQueue )
application . GetPrimaryBus ( ) . Subscribe ( event . DoneStorageMigration , eh . appBusQueue )
}
func ( eh * EventHandler ) GetNextEvent ( ) string {
appChan := eh . appBusQueue . OutChan ( )
select {
case e := <- appChan :
return eh . handleAppBusEvent ( & e )
default :
select {
case e := <- appChan :
return eh . handleAppBusEvent ( & e )
case ev := <- eh . profileEvents :
return eh . handleProfileEvent ( & ev )
}
}
}
// track acnStatus across events
var acnStatus = - 1
// 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 :
newAcnStatus , err := strconv . Atoi ( e . Data [ event . Progress ] )
if err != nil {
break
}
acnStatus = newAcnStatus
2023-02-28 17:56:09 +00:00
for _ , module := range eh . modules {
module . OnACNStatusEvent ( eh . app , e )
}
2023-02-21 20:31:49 +00:00
case event . NewPeer :
onion := e . Data [ event . Identity ]
profile := eh . app . GetPeer ( e . Data [ event . Identity ] )
if profile == nil {
log . Errorf ( "NewPeer: skipping profile initialization. this should only happen when the app is rapidly opened+closed (eg during testing)" )
break
}
log . Debug ( "New Peer Event: %v" , e )
2023-04-17 17:48:58 +00:00
if e . Data [ ReloadEvent ] != event . True {
2023-02-21 20:31:49 +00:00
eh . startHandlingPeer ( onion )
}
// CwtchPeer will always set this now...
tag , _ := profile . GetScopedZonedAttribute ( attr . LocalScope , attr . ProfileZone , constants . Tag )
e . Data [ constants . Tag ] = tag
if e . Data [ event . Created ] == event . True {
profile . SetScopedZonedAttribute ( attr . PublicScope , attr . ProfileZone , constants2 . Picture , ImageToString ( NewImage ( RandomProfileImage ( onion ) , TypeImageDistro ) ) )
}
profile . SetScopedZonedAttribute ( attr . LocalScope , attr . ProfileZone , constants2 . PeerOnline , event . False )
// disabeling network check for connection attempt reservation, needs rework
//eh.app.AddPeerPlugin(onion, plugins.NETWORKCHECK)
eh . app . AddPeerPlugin ( onion , plugins . ANTISPAM )
// If the user has chosen to block unknown profiles
// then explicitly configure the protocol engine to do so..
settings := eh . app . ReadSettings ( )
if settings . BlockUnknownConnections {
profile . BlockUnknownConnections ( )
} else {
// For completeness
profile . AllowUnknownConnections ( )
}
// Start up the Profile
if acnStatus == 100 {
2023-09-13 18:50:13 +00:00
autostart , exists := profile . GetScopedZonedAttribute ( attr . LocalScope , attr . ProfileZone , constants . PeerAutostart )
appearOffline , appearOfflineExists := profile . GetScopedZonedAttribute ( attr . LocalScope , attr . ProfileZone , constants . PeerAppearOffline )
2023-02-21 20:31:49 +00:00
if ! exists || autostart == "true" {
eh . app . ActivatePeerEngine ( onion )
2023-09-13 18:50:13 +00:00
if appearOfflineExists && appearOffline == "true" {
// don't configure any connections...
} else {
eh . app . ConfigureConnections ( onion , true , true , true )
}
2023-02-21 20:31:49 +00:00
}
}
online , _ := profile . GetScopedZonedAttribute ( attr . LocalScope , attr . ProfileZone , constants2 . PeerOnline )
e . Data [ "Online" ] = online
autostart , exists := profile . GetScopedZonedAttribute ( attr . LocalScope , attr . ProfileZone , constants2 . PeerAutostart )
// legacy profiles should autostart by default
if ! exists {
autostart = "true"
}
e . Data [ "autostart" ] = autostart
2023-09-13 18:52:11 +00:00
appearOffline , exists := profile . GetScopedZonedAttribute ( attr . LocalScope , attr . ProfileZone , constants . PeerAppearOffline )
2023-09-13 18:50:13 +00:00
// legacy profiles should not appearOffline by default
if ! exists {
appearOffline = "false"
}
e . Data [ "appearOffline" ] = appearOffline
2023-02-21 20:31:49 +00:00
// Name always exists
e . Data [ constants . Name ] , _ = profile . GetScopedZonedAttribute ( attr . PublicScope , attr . ProfileZone , constants . Name )
e . Data [ constants2 . DefaultProfilePicture ] = RandomProfileImage ( onion )
// if a custom profile image exists then default to it.
key , exists := profile . GetScopedZonedAttribute ( attr . PublicScope , attr . ProfileZone , constants . CustomProfileImageKey )
if ! exists {
e . Data [ constants2 . Picture ] = RandomProfileImage ( onion )
} else {
e . Data [ constants2 . Picture ] , _ = profile . GetScopedZonedAttribute ( attr . LocalScope , attr . FilesharingZone , fmt . Sprintf ( "%s.path" , key ) )
}
// Construct our conversations and our srever lists
var contacts [ ] Contact
2023-07-25 21:45:10 +00:00
var knownServers [ ] servers . Server
2023-02-21 20:31:49 +00:00
conversations , err := profile . FetchConversations ( )
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 ( ) {
2023-07-25 21:45:10 +00:00
groupHandler := servers . FunctionalityGate ( )
2023-02-21 20:31:49 +00:00
if err == nil {
2024-01-15 17:54:26 +00:00
serverInfo , _ := groupHandler . GetServerInfo ( profile , conversationInfo . Handle )
2024-01-03 00:06:38 +00:00
knownServers = append ( knownServers , serverInfo )
2023-02-21 20:31:49 +00:00
}
continue
}
// Prefer local override to public name...
2023-04-20 22:37:06 +00:00
localname , exists := conversationInfo . GetAttribute ( attr . LocalScope , attr . ProfileZone , constants . Name )
2023-02-21 20:31:49 +00:00
if ! exists {
2023-04-20 22:37:06 +00:00
localname = ""
}
name , exists := conversationInfo . GetAttribute ( attr . PublicScope , attr . ProfileZone , constants . Name )
if ! exists {
name = conversationInfo . Handle
2023-02-21 20:31:49 +00:00
}
// Resolve the profile image of the contact
var cpicPath string
var defaultPath string
if conversationInfo . IsGroup ( ) {
cpicPath = RandomGroupImage ( conversationInfo . Handle )
defaultPath = RandomGroupImage ( conversationInfo . Handle )
} else {
2024-02-23 23:22:12 +00:00
cpicPath = eh . GetProfileImage ( profile , conversationInfo , settings . DownloadPath )
2023-02-21 20:31:49 +00:00
defaultPath = RandomProfileImage ( conversationInfo . Handle )
}
// Resolve Save History Setting
saveHistory , set := conversationInfo . GetAttribute ( attr . LocalScope , attr . ProfileZone , event . SaveHistoryKey )
if ! set {
saveHistory = event . DeleteHistoryDefault
}
// Resolve Archived Setting
isArchived , set := conversationInfo . GetAttribute ( attr . LocalScope , attr . ProfileZone , constants2 . Archived )
if ! set {
isArchived = event . False
}
unread := 0
lastSeenMessageId := - 1
lastSeenTimeStr , set := conversationInfo . GetAttribute ( attr . LocalScope , attr . ProfileZone , constants2 . LastSeenTime )
if set {
lastSeenTime , err := time . Parse ( constants2 . DartIso8601 , lastSeenTimeStr )
if err == nil {
// get last 100 messages and count how many are after the lastSeenTime (100 cus hte ui just shows 99+ after)
messages , err := profile . GetMostRecentMessages ( conversationInfo . ID , 0 , 0 , 100 )
if err == nil {
for _ , message := range messages {
msgTime , err := time . Parse ( time . RFC3339Nano , message . Attr [ constants . AttrSentTimestamp ] )
if err == nil {
if msgTime . UTC ( ) . After ( lastSeenTime . UTC ( ) ) {
unread ++
} else {
lastSeenMessageId = message . ID
break
}
}
}
}
}
}
groupServer , _ := conversationInfo . GetAttribute ( attr . LocalScope , attr . LegacyGroupZone , constants . GroupServer )
stateHandle := conversationInfo . Handle
if conversationInfo . IsGroup ( ) {
stateHandle = groupServer
}
state := profile . GetPeerState ( stateHandle )
if ! set {
state = connections . DISCONNECTED
}
blocked := false
if conversationInfo . ACL [ conversationInfo . Handle ] . Blocked {
blocked = true
}
// 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 )
notificationPolicy := constants2 . ConversationNotificationPolicyDefault
if notificationPolicyAttr , exists := conversationInfo . GetAttribute ( attr . LocalScope , attr . ProfileZone , constants2 . ConversationNotificationPolicy ) ; exists {
notificationPolicy = notificationPolicyAttr
}
contacts = append ( contacts , Contact {
Name : name ,
2023-04-20 22:37:06 +00:00
LocalName : localname ,
2023-02-21 20:31:49 +00:00
Identifier : conversationInfo . ID ,
Onion : conversationInfo . Handle ,
Status : connections . ConnectionStateName [ state ] ,
Picture : cpicPath ,
DefaultPicture : defaultPath ,
Accepted : conversationInfo . Accepted ,
AccessControlList : conversationInfo . ACL ,
Blocked : blocked ,
SaveHistory : saveHistory ,
Messages : count ,
Unread : unread ,
LastSeenMessageId : lastSeenMessageId ,
LastMessage : strconv . Itoa ( getLastMessageTime ( lastMessage ) ) ,
IsGroup : conversationInfo . IsGroup ( ) ,
GroupServer : groupServer ,
IsArchived : isArchived == event . True ,
NotificationPolicy : notificationPolicy ,
Attributes : conversationInfo . Attributes ,
} )
}
}
bytes , _ := json . Marshal ( contacts )
e . Data [ "ContactsJson" ] = string ( bytes )
// Marshal the server list into the new peer event...
2023-07-25 21:45:10 +00:00
serversListBytes , _ := json . Marshal ( knownServers )
e . Data [ servers . ServerList ] = string ( serversListBytes )
2023-02-21 20:31:49 +00:00
log . Debugf ( "contactsJson %v" , e . Data [ "ContactsJson" ] )
}
}
json , _ := json . Marshal ( e )
return string ( json )
}
2024-02-23 23:22:12 +00:00
func ( eh * EventHandler ) GetProfileImage ( profile peer . CwtchPeer , conversationInfo * model . Conversation , basepath string ) string {
if eh . app . IsFeatureEnabled ( constants . ImagePreviewsExperiment ) {
fileKey , err := profile . GetConversationAttribute ( conversationInfo . ID , attr . PublicScope . ConstructScopedZonedPath ( attr . ProfileZone . ConstructZonedPath ( constants . CustomProfileImageKey ) ) )
if err == nil {
if value , exists := profile . GetScopedZonedAttribute ( attr . LocalScope , attr . FilesharingZone , fmt . Sprintf ( "%s.complete" , fileKey ) ) ; exists && value == event . True {
fp , _ := filesharing . GenerateDownloadPath ( basepath , fileKey , true )
// check if the file exists...if it does then set the path...
if _ , err := os . Stat ( fp ) ; err == nil {
image , _ := profile . GetScopedZonedAttribute ( attr . LocalScope , attr . FilesharingZone , fmt . Sprintf ( "%s.path" , fileKey ) )
return image
}
2023-02-21 20:31:49 +00:00
}
}
}
return RandomProfileImage ( conversationInfo . Handle )
}
// handleProfileEvent enriches Profile events so they are usable with out further data fetches
func ( eh * EventHandler ) handleProfileEvent ( ev * EventProfileEnvelope ) string {
// cache of contact states to use to filter out events repeating known states
var contactStateCache = make ( map [ string ] connections . ConnectionState )
if eh . app == nil {
log . Errorf ( "eh.app == nil in handleProfileEvent... this shouldnt happen?" )
} else {
profile := eh . app . GetPeer ( ev . Profile )
2024-01-15 17:54:26 +00:00
if profile == nil {
log . Errorf ( "something has gone very wrong. profile is nil in handleProfileEvent" )
return ""
}
2023-02-21 20:31:49 +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
ci , err := profile . FetchConversationInfo ( ev . Event . Data [ "RemotePeer" ] )
ev . Event . Data [ constants2 . Picture ] = RandomProfileImage ( 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 )
2024-02-23 23:22:12 +00:00
ev . Event . Data [ constants2 . Picture ] = eh . GetProfileImage ( profile , ci , eh . app . ReadSettings ( ) . DownloadPath )
2023-02-21 20:31:49 +00:00
} else {
// TODO This Conversation May Not Exist Yet...But we are not in charge of creating it...
log . Errorf ( "todo wait for contact to be added before processing this event..." )
return ""
}
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" ]
}
}
ev . Event . Data [ "notification" ] = string ( determineNotification ( ci , eh . app . ReadSettings ( ) ) )
case event . NewMessageFromGroup :
// only needs contact nickname and picture, for displaying on popup notifications
ci , err := profile . FetchConversationInfo ( ev . Event . Data [ "RemotePeer" ] )
ev . Event . Data [ constants2 . Picture ] = RandomProfileImage ( ev . Event . Data [ "RemotePeer" ] )
if ci != nil && err == nil {
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" ]
}
}
2024-02-23 23:22:12 +00:00
ev . Event . Data [ constants2 . Picture ] = eh . GetProfileImage ( profile , ci , eh . app . ReadSettings ( ) . DownloadPath )
2023-02-21 20:31:49 +00:00
}
conversationID , _ := strconv . Atoi ( ev . Event . Data [ event . ConversationID ] )
profile . SetConversationAttribute ( conversationID , attr . LocalScope . ConstructScopedZonedPath ( attr . ProfileZone . ConstructZonedPath ( constants2 . Archived ) ) , event . False )
2024-01-15 17:54:26 +00:00
gci , err := profile . GetConversationInfo ( conversationID )
if err != nil {
log . Errorf ( "new message from non-existant group: %v" , err )
break
}
2023-02-21 20:31:49 +00:00
groupServer := gci . Attributes [ attr . LocalScope . ConstructScopedZonedPath ( attr . LegacyGroupZone . ConstructZonedPath ( constants . GroupServer ) ) . ToString ( ) ]
state := profile . GetPeerState ( groupServer )
// if syncing, don't flood with notifications
if state == connections . SYNCED {
ev . Event . Data [ "notification" ] = string ( determineNotification ( gci , eh . app . ReadSettings ( ) ) )
} else {
ev . Event . Data [ "notification" ] = string ( constants2 . NotificationNone )
}
case event . PeerAcknowledgement :
ci , err := profile . FetchConversationInfo ( ev . Event . Data [ "RemotePeer" ] )
if ci != nil && err == nil {
ev . Event . Data [ event . ConversationID ] = strconv . Itoa ( ci . ID )
}
case event . ContactCreated :
conversationID , _ := strconv . Atoi ( ev . Event . Data [ event . ConversationID ] )
count , err := profile . GetChannelMessageCount ( conversationID , 0 )
if err != nil {
log . Errorf ( "error fetching channel message count %v %v" , conversationID , err )
2024-01-15 17:54:26 +00:00
break
2023-02-21 20:31:49 +00:00
}
conversationInfo , err := profile . GetConversationInfo ( conversationID )
if err != nil {
log . Errorf ( "error fetching conversation info for %v %v" , conversationID , err )
2024-01-15 17:54:26 +00:00
break
2023-02-21 20:31:49 +00:00
}
blocked := constants . False
if conversationInfo . ACL [ conversationInfo . Handle ] . Blocked {
blocked = constants . True
}
accepted := constants . False
if conversationInfo . Accepted {
accepted = constants . True
}
2024-01-15 17:54:26 +00:00
acl , err := json . Marshal ( conversationInfo . ACL )
if err != nil {
log . Errorf ( "received invalid ACL in conversation: %v" , err )
break
}
2023-02-21 20:31:49 +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 [ constants2 . Picture ] = RandomProfileImage ( conversationInfo . Handle )
ev . Event . Data [ constants2 . DefaultProfilePicture ] = RandomProfileImage ( conversationInfo . Handle )
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 [ "accepted" ] = accepted
ev . Event . Data [ "accessControlList" ] = string ( acl )
ev . Event . Data [ "blocked" ] = blocked
ev . Event . Data [ "loading" ] = "false"
ev . Event . Data [ "lastMsgTime" ] = strconv . Itoa ( getLastMessageTime ( lastMessage ) )
case event . GroupCreated :
// This event should only happen after we have validated the invite, as such the error
// condition *should* never happen.
groupPic := RandomGroupImage ( ev . Event . Data [ event . GroupID ] )
ev . Event . Data [ constants2 . Picture ] = groupPic
2024-01-15 17:54:26 +00:00
conversationID , err := strconv . Atoi ( ev . Event . Data [ event . ConversationID ] )
if err != nil {
log . Errorf ( "invalid conversation id recieved %v" , err )
break
}
conversationInfo , err := profile . GetConversationInfo ( conversationID )
if err != nil {
log . Errorf ( "error fetching conversation info for %v %v" , conversationID , err )
break
}
2023-02-21 20:31:49 +00:00
acl , _ := json . Marshal ( conversationInfo . ACL )
ev . Event . Data [ "accessControlList" ] = string ( acl )
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 {
groupPic := RandomGroupImage ( invite . GroupID )
ev . Event . Data [ constants2 . Picture ] = groupPic
2024-01-15 17:54:26 +00:00
conversationID , err := strconv . Atoi ( ev . Event . Data [ event . ConversationID ] )
if err != nil {
log . Errorf ( "invalid conversation id recieved %v" , err )
break
}
conversationInfo , err := profile . GetConversationInfo ( conversationID )
if err != nil {
log . Errorf ( "error fetching conversation info for %v %v" , conversationID , err )
break
}
acl , err := json . Marshal ( conversationInfo . ACL )
2023-02-21 20:31:49 +00:00
ev . Event . Data [ "accessControlList" ] = string ( acl )
2024-01-15 17:54:26 +00:00
if err != nil {
log . Errorf ( "received invalid ACL in conversation: %v" , err )
break
}
2023-02-21 20:31:49 +00:00
} 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.openprivacy.ca/cwtch.im/cwtch" , err )
return ""
}
case event . PeerStateChange :
cxnState := connections . ConnectionStateToType ( ) [ ev . Event . Data [ event . ConnectionState ] ]
if ev . Event . Data [ event . RemotePeer ] == profile . GetOnion ( ) {
return "" // suppress events from our own profile...
}
contactStateCache [ ev . Event . Data [ event . RemotePeer ] ] = cxnState
case event . ServerStateChange :
cxnState := connections . ConnectionStateToType ( ) [ ev . Event . Data [ event . ConnectionState ] ]
contactStateCache [ ev . Event . Data [ event . RemotePeer ] ] = cxnState
case event . TokenManagerInfo :
conversations , err := profile . FetchConversations ( )
if err == nil {
var associatedGroups [ ] int
for _ , ci := range conversations {
groupServer , groupServerExists := ci . Attributes [ attr . LocalScope . ConstructScopedZonedPath ( attr . LegacyGroupZone . ConstructZonedPath ( constants . GroupServer ) ) . ToString ( ) ]
if groupServerExists {
gci , err := profile . FetchConversationInfo ( groupServer )
if err == nil {
tokenOnion , onionExists := gci . Attributes [ attr . PublicScope . ConstructScopedZonedPath ( attr . ServerKeyZone . ConstructZonedPath ( string ( model . KeyTypeTokenOnion ) ) ) . ToString ( ) ]
if onionExists && tokenOnion == ev . Event . Data [ event . ServerTokenOnion ] {
associatedGroups = append ( associatedGroups , ci . ID )
}
}
}
}
associatedGroupsJson , _ := json . Marshal ( associatedGroups )
ev . Event . Data [ event . Data ] = string ( associatedGroupsJson )
}
case event . ProtocolEngineCreated :
// TODO this code should be moved into Cwtch during the API officialization...
settings := eh . app . ReadSettings ( )
// ensure that protocol engine respects blocking settings...
if settings . BlockUnknownConnections {
profile . BlockUnknownConnections ( )
} else {
profile . AllowUnknownConnections ( )
}
}
}
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 )
2024-01-15 17:54:26 +00:00
if eventBus == nil {
log . Errorf ( "could not start handling peer events .. event bus is nil" )
return
}
2023-02-21 20:31:49 +00:00
q := event . NewQueue ( )
eventBus . Subscribe ( event . NetworkStatus , q )
eventBus . Subscribe ( event . ACNInfo , q )
eventBus . Subscribe ( event . NewMessageFromPeer , q )
eventBus . Subscribe ( event . UpdatedProfileAttribute , q )
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 )
eventBus . Subscribe ( event . NewMessageFromGroup , q )
eventBus . Subscribe ( event . GroupCreated , q )
eventBus . Subscribe ( event . NewGroup , q )
eventBus . Subscribe ( event . ServerStateChange , q )
eventBus . Subscribe ( event . PeerStateChange , q )
2023-04-20 22:28:28 +00:00
eventBus . Subscribe ( event . UpdatedConversationAttribute , q )
2023-02-21 20:31:49 +00:00
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 )
eventBus . Subscribe ( event . TokenManagerInfo , q )
eventBus . Subscribe ( event . ProtocolEngineCreated , q )
2023-07-25 21:45:10 +00:00
eventBus . Subscribe ( event . SearchResult , q )
eventBus . Subscribe ( event . SearchCancelled , q )
2023-07-25 21:54:59 +00:00
eventBus . Subscribe ( servers . UpdateServerInfo , q )
2023-02-21 20:31:49 +00:00
go eh . forwardProfileMessages ( onion , q )
}
func ( eh * EventHandler ) forwardProfileMessages ( onion string , q event . Queue ) {
log . Infof ( "Launching Forwarding Goroutine" )
// 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
}
}
}
// Push pushes an event onto the app event bus
//
// It is also a way for libCwtch-go to publish an event for consumption by a UI before a Cwtch app has been initialized
// use: to signal an error before a cwtch app could be created
func ( eh * EventHandler ) Push ( newEvent event . Event ) {
eh . appBusQueue . Publish ( newEvent )
}
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 ( ) )
}