2024-02-19 09:52:22 +00:00
package communities
import (
"crypto/ecdsa"
"errors"
"sort"
"github.com/golang/protobuf/proto"
"go.uber.org/zap"
utils "github.com/status-im/status-go/common"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/protobuf"
)
var ErrInvalidCommunityEventClock = errors . New ( "clock for admin event message is outdated" )
func ( o * Community ) processEvents ( message * CommunityEventsMessage , lastlyAppliedEvents map [ string ] uint64 ) error {
processor := & eventsProcessor {
community : o ,
message : message ,
logger : o . config . Logger . Named ( "eventsProcessor" ) ,
lastlyAppliedEvents : lastlyAppliedEvents ,
}
return processor . exec ( )
}
type eventsProcessor struct {
community * Community
message * CommunityEventsMessage
logger * zap . Logger
lastlyAppliedEvents map [ string ] uint64
eventsToApply [ ] CommunityEvent
}
func ( e * eventsProcessor ) exec ( ) error {
e . community . mutex . Lock ( )
defer e . community . mutex . Unlock ( )
err := e . validateDescription ( )
if err != nil {
return err
}
e . filterEvents ( )
e . mergeEvents ( )
e . retainNewestEventsPerEventTypeID ( )
e . sortEvents ( )
e . applyEvents ( )
return nil
}
func ( e * eventsProcessor ) validateDescription ( ) error {
description , err := validateAndGetEventsMessageCommunityDescription ( e . message . EventsBaseCommunityDescription , e . community . ControlNode ( ) )
if err != nil {
return err
}
// Control node is the only entity that can apply events from past description.
// In this case, events are compared against the clocks of the most recently applied events.
if e . community . IsControlNode ( ) && description . Clock < e . community . config . CommunityDescription . Clock {
return nil
}
if description . Clock != e . community . config . CommunityDescription . Clock {
return ErrInvalidCommunityEventClock
}
return nil
}
2024-03-25 19:02:38 +00:00
func ( e * eventsProcessor ) validateEvent ( event * CommunityEvent ) error {
if e . lastlyAppliedEvents != nil {
if clock , found := e . lastlyAppliedEvents [ event . EventTypeID ( ) ] ; found && clock >= event . CommunityEventClock {
return errors . New ( "event outdated" )
2024-02-19 09:52:22 +00:00
}
2024-03-25 19:02:38 +00:00
}
2024-02-19 09:52:22 +00:00
2024-03-25 19:02:38 +00:00
signer , err := event . RecoverSigner ( )
if err != nil {
return err
2024-02-19 09:52:22 +00:00
}
2024-03-25 19:02:38 +00:00
return e . community . validateEvent ( event , signer )
}
2024-02-19 09:52:22 +00:00
2024-03-25 19:02:38 +00:00
// Filter invalid and outdated events.
func ( e * eventsProcessor ) filterEvents ( ) {
for _ , ev := range e . message . Events {
event := ev
if err := e . validateEvent ( & event ) ; err == nil {
2024-02-19 09:52:22 +00:00
e . eventsToApply = append ( e . eventsToApply , event )
} else {
e . logger . Warn ( "invalid community event" , zap . String ( "EventTypeID" , event . EventTypeID ( ) ) , zap . Uint64 ( "clock" , event . CommunityEventClock ) , zap . Error ( err ) )
}
}
}
// Merge message's events with community's events.
func ( e * eventsProcessor ) mergeEvents ( ) {
if e . community . config . EventsData != nil {
2024-03-25 19:02:38 +00:00
for _ , ev := range e . community . config . EventsData . Events {
event := ev
if err := e . validateEvent ( & event ) ; err == nil {
e . eventsToApply = append ( e . eventsToApply , event )
} else {
// NOTE: this should not happen, events should be validated before they are saved in the db.
// It has been identified that an invalid event is saved to the database for some reason.
// The code flow leading to this behavior is not yet known.
// https://github.com/status-im/status-desktop/issues/14106
e . logger . Error ( "invalid community event read from db" , zap . String ( "EventTypeID" , event . EventTypeID ( ) ) , zap . Uint64 ( "clock" , event . CommunityEventClock ) , zap . Error ( err ) )
}
}
2024-02-19 09:52:22 +00:00
}
}
// Keep only the newest event per PropertyTypeID.
func ( e * eventsProcessor ) retainNewestEventsPerEventTypeID ( ) {
eventsMap := make ( map [ string ] CommunityEvent )
for _ , event := range e . eventsToApply {
if existingEvent , found := eventsMap [ event . EventTypeID ( ) ] ; ! found || event . CommunityEventClock > existingEvent . CommunityEventClock {
eventsMap [ event . EventTypeID ( ) ] = event
}
}
e . eventsToApply = [ ] CommunityEvent { }
for _ , event := range eventsMap {
e . eventsToApply = append ( e . eventsToApply , event )
}
}
// Sorts events by clock.
func ( e * eventsProcessor ) sortEvents ( ) {
sort . Slice ( e . eventsToApply , func ( i , j int ) bool {
if e . eventsToApply [ i ] . CommunityEventClock == e . eventsToApply [ j ] . CommunityEventClock {
return e . eventsToApply [ i ] . Type < e . eventsToApply [ j ] . Type
}
return e . eventsToApply [ i ] . CommunityEventClock < e . eventsToApply [ j ] . CommunityEventClock
} )
}
func ( e * eventsProcessor ) applyEvents ( ) {
if e . community . config . EventsData == nil {
e . community . config . EventsData = & EventsData {
EventsBaseCommunityDescription : e . message . EventsBaseCommunityDescription ,
}
}
e . community . config . EventsData . Events = e . eventsToApply
e . community . applyEvents ( )
}
func ( o * Community ) applyEvents ( ) {
if o . config . EventsData == nil {
return
}
for _ , event := range o . config . EventsData . Events {
err := o . applyEvent ( event )
if err != nil {
o . config . Logger . Warn ( "failed to apply event" , zap . String ( "EventTypeID" , event . EventTypeID ( ) ) , zap . Uint64 ( "clock" , event . CommunityEventClock ) , zap . Error ( err ) )
}
}
}
func ( o * Community ) applyEvent ( communityEvent CommunityEvent ) error {
switch communityEvent . Type {
case protobuf . CommunityEvent_COMMUNITY_EDIT :
o . config . CommunityDescription . Identity = communityEvent . CommunityConfig . Identity
o . config . CommunityDescription . Permissions = communityEvent . CommunityConfig . Permissions
o . config . CommunityDescription . AdminSettings = communityEvent . CommunityConfig . AdminSettings
o . config . CommunityDescription . IntroMessage = communityEvent . CommunityConfig . IntroMessage
o . config . CommunityDescription . OutroMessage = communityEvent . CommunityConfig . OutroMessage
o . config . CommunityDescription . Tags = communityEvent . CommunityConfig . Tags
case protobuf . CommunityEvent_COMMUNITY_MEMBER_TOKEN_PERMISSION_CHANGE :
if o . IsControlNode ( ) {
_ , err := o . upsertTokenPermission ( communityEvent . TokenPermission )
if err != nil {
return err
}
}
case protobuf . CommunityEvent_COMMUNITY_MEMBER_TOKEN_PERMISSION_DELETE :
if o . IsControlNode ( ) {
_ , err := o . deleteTokenPermission ( communityEvent . TokenPermission . Id )
if err != nil {
return err
}
}
case protobuf . CommunityEvent_COMMUNITY_CATEGORY_CREATE :
_ , err := o . createCategory ( communityEvent . CategoryData . CategoryId , communityEvent . CategoryData . Name , communityEvent . CategoryData . ChannelsIds )
if err != nil {
return err
}
case protobuf . CommunityEvent_COMMUNITY_CATEGORY_DELETE :
_ , err := o . deleteCategory ( communityEvent . CategoryData . CategoryId )
if err != nil {
return err
}
case protobuf . CommunityEvent_COMMUNITY_CATEGORY_EDIT :
_ , err := o . editCategory ( communityEvent . CategoryData . CategoryId , communityEvent . CategoryData . Name , communityEvent . CategoryData . ChannelsIds )
if err != nil {
return err
}
case protobuf . CommunityEvent_COMMUNITY_CHANNEL_CREATE :
err := o . createChat ( communityEvent . ChannelData . ChannelId , communityEvent . ChannelData . Channel )
if err != nil {
return err
}
case protobuf . CommunityEvent_COMMUNITY_CHANNEL_DELETE :
o . deleteChat ( communityEvent . ChannelData . ChannelId )
case protobuf . CommunityEvent_COMMUNITY_CHANNEL_EDIT :
err := o . editChat ( communityEvent . ChannelData . ChannelId , communityEvent . ChannelData . Channel )
if err != nil {
return err
}
case protobuf . CommunityEvent_COMMUNITY_CHANNEL_REORDER :
_ , err := o . reorderChat ( communityEvent . ChannelData . CategoryId , communityEvent . ChannelData . ChannelId , int ( communityEvent . ChannelData . Position ) )
if err != nil {
return err
}
case protobuf . CommunityEvent_COMMUNITY_CATEGORY_REORDER :
_ , err := o . reorderCategories ( communityEvent . CategoryData . CategoryId , int ( communityEvent . CategoryData . Position ) )
if err != nil {
return err
}
case protobuf . CommunityEvent_COMMUNITY_MEMBER_KICK :
if o . IsControlNode ( ) {
pk , err := common . HexToPubkey ( communityEvent . MemberToAction )
if err != nil {
return err
}
o . removeMemberFromOrg ( pk )
}
case protobuf . CommunityEvent_COMMUNITY_MEMBER_BAN :
if o . IsControlNode ( ) {
pk , err := common . HexToPubkey ( communityEvent . MemberToAction )
if err != nil {
return err
}
2024-02-22 10:25:13 +00:00
o . banUserFromCommunity ( pk , & protobuf . CommunityBanInfo { DeleteAllMessages : false } )
2024-02-19 09:52:22 +00:00
}
case protobuf . CommunityEvent_COMMUNITY_MEMBER_UNBAN :
if o . IsControlNode ( ) {
pk , err := common . HexToPubkey ( communityEvent . MemberToAction )
if err != nil {
return err
}
o . unbanUserFromCommunity ( pk )
}
case protobuf . CommunityEvent_COMMUNITY_TOKEN_ADD :
o . config . CommunityDescription . CommunityTokensMetadata = append ( o . config . CommunityDescription . CommunityTokensMetadata , communityEvent . TokenMetadata )
2024-02-29 17:54:17 +00:00
case protobuf . CommunityEvent_COMMUNITY_DELETE_BANNED_MEMBER_MESSAGES :
if o . IsControlNode ( ) {
pk , err := common . HexToPubkey ( communityEvent . MemberToAction )
if err != nil {
return err
}
err = o . deleteBannedMemberAllMessages ( pk )
if err != nil {
return err
}
}
2024-02-19 09:52:22 +00:00
}
return nil
}
func ( o * Community ) addNewCommunityEvent ( event * CommunityEvent ) error {
err := event . Validate ( )
if err != nil {
return err
}
// All events must be built on top of the control node CommunityDescription
// If there were no events before, extract CommunityDescription from CommunityDescriptionProtocolMessage
// and check the signature
if o . config . EventsData == nil || len ( o . config . EventsData . EventsBaseCommunityDescription ) == 0 {
_ , err := validateAndGetEventsMessageCommunityDescription ( o . config . CommunityDescriptionProtocolMessage , o . ControlNode ( ) )
if err != nil {
return err
}
o . config . EventsData = & EventsData {
EventsBaseCommunityDescription : o . config . CommunityDescriptionProtocolMessage ,
Events : [ ] CommunityEvent { } ,
}
}
event . Payload , err = proto . Marshal ( event . ToProtobuf ( ) )
if err != nil {
return err
}
o . config . EventsData . Events = append ( o . config . EventsData . Events , * event )
return nil
}
func ( o * Community ) toCommunityEventsMessage ( ) * CommunityEventsMessage {
return & CommunityEventsMessage {
CommunityID : o . ID ( ) ,
EventsBaseCommunityDescription : o . config . EventsData . EventsBaseCommunityDescription ,
Events : o . config . EventsData . Events ,
}
}
func validateAndGetEventsMessageCommunityDescription ( signedDescription [ ] byte , signerPubkey * ecdsa . PublicKey ) ( * protobuf . CommunityDescription , error ) {
metadata := & protobuf . ApplicationMetadataMessage { }
err := proto . Unmarshal ( signedDescription , metadata )
if err != nil {
return nil , err
}
if metadata . Type != protobuf . ApplicationMetadataMessage_COMMUNITY_DESCRIPTION {
return nil , ErrInvalidMessage
}
signer , err := utils . RecoverKey ( metadata )
if err != nil {
return nil , err
}
if signer == nil {
return nil , errors . New ( "CommunityDescription does not contain the control node signature" )
}
if ! signer . Equal ( signerPubkey ) {
return nil , errors . New ( "CommunityDescription was not signed by an owner" )
}
description := & protobuf . CommunityDescription { }
err = proto . Unmarshal ( metadata . Payload , description )
if err != nil {
return nil , err
}
return description , nil
}