X-Git-Url: https://git.giacomolli.uk/onechat/blobdiff_plain/48d7bce28b18ecef4c52c98821675605464cecb9..6c7859cfea123479aba046614ee81028651ddaf5:/whatsapp_state.go?ds=sidebyside diff --git a/whatsapp_state.go b/whatsapp_state.go index 41bd2f0..6114b28 100644 --- a/whatsapp_state.go +++ b/whatsapp_state.go @@ -16,12 +16,12 @@ const ( // the batches that are not in the log yet and the records of the sends. whatsappStateFileName = "state.json" - // whatsappBatchQuiet is how long a chat is quiet before its batch + // whatsappBatchIdleTimeout is how long a chat is quiet before its batch // closes. - whatsappBatchQuiet = 20 * time.Second + whatsappBatchIdleTimeout = 20 * time.Second - // whatsappBatchWindow is the longest time of a batch. - whatsappBatchWindow = 2 * time.Minute + // whatsappBatchTimeout is the longest time of a batch. + whatsappBatchTimeout = 2 * time.Minute // whatsappBatchMessages is the most messages of a batch. whatsappBatchMessages = 50 @@ -31,9 +31,60 @@ const ( whatsappSendRecords = 1000 ) +// WhatsAppState holds the state file: the send records, and the batches of the +// WhatsApp messages that are not in the log yet, open and closed, in the order +// of their first message. The messages of one chat form one batch while the +// times of the messages are at most whatsappBatchIdleTimeout apart, at most +// whatsappBatchTimeout after the first, and the batch has fewer than +// whatsappBatchMessages messages. The times are the WhatsApp times, so a +// catch-up after a stop gives the same batches as live messages. A batch also +// closes when whatsappBatchIdleTimeout passes after the arrival of its last +// message, or whatsappBatchTimeout after the arrival of its first message, in +// real time. +// +// A send record keeps the result of a send by its ID, so that a repeated call +// of the send tool does not send again. The state file keeps the last +// whatsappSendRecords records. +// +// Each change writes the state file whole, so the batches and the records +// survive a restart. The mutex protects them, because the event handler of the +// library adds messages while Run closes and removes batches, and the turns +// send. +type WhatsAppState struct { + path string + + mutex sync.Mutex + file WhatsAppStateFile +} + +// WhatsAppStateFile is the content of the state file: the number of the last +// batch, the batches that are not in the log yet, and the send records, oldest +// first. +type WhatsAppStateFile struct { + Next int `json:"next"` + Batches []WhatsAppBatch `json:"batches"` + Sends []WhatsAppSend `json:"sends"` +} + +// WhatsAppSend is the record of one send: the ID that the agent gave, the chat, +// the SHA-256 of the text, an optional stored image name, the WhatsApp message +// ID, the state, which is sending, sent, or failed, the answer of the tool, and +// the time of its start. +type WhatsAppSend struct { + ID string `json:"id"` + Chat string `json:"chat"` + Digest string `json:"digest"` + Image string `json:"image,omitempty"` + MessageID string `json:"message_id"` + State string `json:"state"` + Answer string `json:"answer,omitempty"` + Time time.Time `json:"time"` +} + // openWhatsAppState reads the batches and the send records of the state file at -// path. A missing file holds none. A batch closes after whatsappBatchQuiet with -// no message, or after whatsappBatchWindow at the latest. +// path. A missing file holds none. A batch closes after +// whatsappBatchIdleTimeout with no message, or after whatsappBatchTimeout at +// the latest. func openWhatsAppState(path string) (*WhatsAppState, error) { s := &WhatsAppState{ path: path, @@ -56,31 +107,6 @@ func openWhatsAppState(path string) (*WhatsAppState, error) { return s, nil } -// WhatsAppState holds the state file: the send records, and the batches of the -// WhatsApp messages that are not in the log yet, open and closed, in the order -// of their first message. The messages of one chat form one batch while the -// times of the messages are at most whatsappBatchQuiet apart, at most -// whatsappBatchWindow after the first, and the batch has fewer than -// whatsappBatchMessages messages. The times are the WhatsApp times, so a -// catch-up after a stop gives the same batches as live messages. A batch also -// closes when whatsappBatchQuiet passes after the arrival of its last message, -// or whatsappBatchWindow after the arrival of its first message, in real time. -// -// A send record keeps the result of a send by its ID, so that a repeated call -// of the send tool does not send again. The state file keeps the last -// whatsappSendRecords records. -// -// Each change writes the state file whole, so the batches and the records -// survive a restart. The mutex protects them, because the event handler of the -// library adds messages while Run closes and removes batches, and the turns -// send. -type WhatsAppState struct { - path string - - mutex sync.Mutex - file WhatsAppStateFile -} - // Add puts message of chat into the open batch of chat, or into a new batch, // with arrival as its time of arrival. A message that the batch holds already, // which the servers sent again, changes nothing. It returns true when a batch @@ -99,7 +125,7 @@ func (s *WhatsAppState) Add(chat WhatsAppChat, message WhatsAppMessage, arrival return false, nil } - if message.Time.Sub(batch.Messages[len(batch.Messages)-1].Time) > whatsappBatchQuiet || message.Time.Sub(batch.Messages[0].Time) > whatsappBatchWindow { + if message.Time.Sub(batch.Messages[len(batch.Messages)-1].Time) > whatsappBatchIdleTimeout || message.Time.Sub(batch.Messages[0].Time) > whatsappBatchTimeout { batch.Closed = true closed = true index = -1 @@ -113,6 +139,7 @@ func (s *WhatsAppState) Add(chat WhatsAppChat, message WhatsAppMessage, arrival Chat: chat, First: arrival, }) + index = len(s.file.Batches) - 1 } @@ -127,6 +154,33 @@ func (s *WhatsAppState) Add(chat WhatsAppChat, message WhatsAppMessage, arrival return closed, s.save() } +// open returns the index of the open batch of chat, or -1. The caller holds the +// mutex. +func (s *WhatsAppState) open(chat string) int { + return slices.IndexFunc(s.file.Batches, func(batch WhatsAppBatch) bool { + return !batch.Closed && batch.Chat.ID == chat + }) +} + +// save writes the state file whole. The caller holds the mutex. +func (s *WhatsAppState) save() error { + data, err := json.Marshal(s.file) + if err != nil { + return fmt.Errorf("encode WhatsApp state: %w", err) + } + + err = os.MkdirAll(filepath.Dir(s.path), 0o700) + if err == nil { + err = writeFileAtomic(s.path, append(data, '\n')) + } + + if err != nil { + return fmt.Errorf("save WhatsApp state: %w", err) + } + + return nil +} + // CloseDue closes each open batch whose time is up at now and tells if one // closed. func (s *WhatsAppState) CloseDue(now time.Time) (bool, error) { @@ -149,6 +203,17 @@ func (s *WhatsAppState) CloseDue(now time.Time) (bool, error) { return true, s.save() } +// deadline returns when batch closes in real time. +func (s *WhatsAppState) deadline(batch WhatsAppBatch) time.Time { + quiet := batch.Last.Add(whatsappBatchIdleTimeout) + window := batch.First.Add(whatsappBatchTimeout) + if window.Before(quiet) { + return window + } + + return quiet +} + // CloseAll closes every open batch. func (s *WhatsAppState) CloseAll() error { s.mutex.Lock() @@ -247,63 +312,3 @@ func (s *WhatsAppState) PutSend(record WhatsAppSend) error { return s.save() } - -// open returns the index of the open batch of chat, or -1. The caller holds the -// mutex. -func (s *WhatsAppState) open(chat string) int { - return slices.IndexFunc(s.file.Batches, func(batch WhatsAppBatch) bool { - return !batch.Closed && batch.Chat.ID == chat - }) -} - -// deadline returns when batch closes in real time. -func (s *WhatsAppState) deadline(batch WhatsAppBatch) time.Time { - quiet := batch.Last.Add(whatsappBatchQuiet) - window := batch.First.Add(whatsappBatchWindow) - if window.Before(quiet) { - return window - } - - return quiet -} - -// save writes the state file whole. The caller holds the mutex. -func (s *WhatsAppState) save() error { - data, err := json.Marshal(s.file) - if err != nil { - return fmt.Errorf("encode WhatsApp state: %w", err) - } - - err = os.MkdirAll(filepath.Dir(s.path), 0o700) - if err == nil { - err = writeFileAtomic(s.path, append(data, '\n')) - } - - if err != nil { - return fmt.Errorf("save WhatsApp state: %w", err) - } - - return nil -} - -// WhatsAppStateFile is the content of the state file: the number of the last -// batch, the batches that are not in the log yet, and the send records, oldest -// first. -type WhatsAppStateFile struct { - Next int `json:"next"` - Batches []WhatsAppBatch `json:"batches"` - Sends []WhatsAppSend `json:"sends"` -} - -// WhatsAppSend is the record of one send: the ID that the agent gave, the chat, -// the SHA-256 of the text, the WhatsApp message ID, the state, which is -// sending, sent, or failed, the answer of the tool, and the time of its start. -type WhatsAppSend struct { - ID string `json:"id"` - Chat string `json:"chat"` - Digest string `json:"digest"` - MessageID string `json:"message_id"` - State string `json:"state"` - Answer string `json:"answer,omitempty"` - Time time.Time `json:"time"` -}