package model import ( "encoding/base64" "sort" "sync" "time" ) // Timeline encapsulates a collection of ordered Messages, and a mechanism to access them // in a threadsafe manner. type Timeline struct { Messages []Message SignedGroupID []byte lock sync.Mutex // a cache to allow quick checks for existing messages... signatureCache map[string]bool } // Message is a local representation of a given message sent over a group chat channel. type Message struct { Timestamp time.Time Received time.Time PeerID string Message string Signature []byte PreviousMessageSig []byte ReceivedByServer bool // messages sent to a server Acknowledged bool // peer to peer Error string `json:",omitempty"` // Application specific flags, useful for storing small amounts of metadata Flags uint64 } // MessageBaseSize 2021.06 byte size of an *empty* message json serialized const MessageBaseSize float64 = 463 // compareSignatures checks if a and b are equal. Note: this function does // not need to be constant time - in fact it is better that it is not as it's only main use // is in sorting timeline state consistently. func compareSignatures(a []byte, b []byte) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } // GetMessages returns a copy of the entire timeline func (t *Timeline) GetMessages() []Message { t.lock.Lock() messages := make([]Message, len(t.Messages)) copy(messages[:], t.Messages[:]) t.lock.Unlock() return messages } // GetCopy returns a duplicate of the Timeline func (t *Timeline) GetCopy() *Timeline { t.lock.Lock() defer t.lock.Unlock() newt := &Timeline{} // initialize the timeline and copy the message over... newt.SetMessages(t.Messages) return newt } // SetMessages sets the Messages of this timeline. Only to be used in loading/initialization func (t *Timeline) SetMessages(messages []Message) { t.lock.Lock() defer t.lock.Unlock() t.init() t.Messages = messages for _, message := range t.Messages { t.signatureCache[base64.StdEncoding.EncodeToString(message.Signature)] = true } } // Len gets the length of the timeline func (t *Timeline) Len() int { return len(t.Messages) } // Swap swaps 2 Messages on the timeline. func (t *Timeline) Swap(i, j int) { t.Messages[i], t.Messages[j] = t.Messages[j], t.Messages[i] } // Less checks 2 Messages (i and j) in the timeline and returns true if i occurred before j, else false func (t *Timeline) Less(i, j int) bool { if t.Messages[i].Timestamp.Before(t.Messages[j].Timestamp) { return true } // Short circuit false if j is before i, signature checks will give a wrong order in this case. if t.Messages[j].Timestamp.Before(t.Messages[i].Timestamp) { return false } if compareSignatures(t.Messages[i].PreviousMessageSig, t.SignedGroupID) { return true } if compareSignatures(t.Messages[i].Signature, t.Messages[j].PreviousMessageSig) { return true } return false } // Sort sorts the timeline in a canonical order. func (t *Timeline) Sort() { t.lock.Lock() defer t.lock.Unlock() sort.Sort(t) } // Insert a message into the timeline in a thread safe way. func (t *Timeline) Insert(mi *Message) bool { t.lock.Lock() defer t.lock.Unlock() // assert timeline is initialized t.init() _, exists := t.signatureCache[base64.StdEncoding.EncodeToString(mi.Signature)] if exists { return true } t.Messages = append(t.Messages, *mi) t.signatureCache[base64.StdEncoding.EncodeToString(mi.Signature)] = true return false } func (t *Timeline) init() { // only allow this setting once... if t.signatureCache == nil { t.signatureCache = make(map[string]bool) } }