From 795ffb66d90ecfdc347b25dfac36ab832dfccef7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Rudowicz?= Date: Mon, 19 Feb 2024 19:52:40 +0100 Subject: [PATCH] Telegram message formatting --- main.go | 26 +++------ message_contents.go | 124 ++++++++++++++++++++++++++++++++++++++++++ sender_worker.go | 28 +++------- sender_worker_test.go | 33 +++++++---- templates.go | 8 +++ templates_test.go | 40 ++++++++++++++ 6 files changed, 209 insertions(+), 50 deletions(-) create mode 100644 message_contents.go create mode 100644 templates.go create mode 100644 templates_test.go diff --git a/main.go b/main.go index 392c70b..3560cd6 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "flag" "fmt" + "html/template" "log" "net" "os" @@ -23,17 +24,12 @@ type TgSender struct { bot *tgbotapi.BotAPI } -func (self TgSender) Send(msg GenericMessage) error { - chatIds := msg.chatIds.GetTgIds() +func (self TgSender) Send(msg GenericMessage, tpl *template.Template) error { + chatIds := msg.ChatIds.GetTgIds() if chatIds == nil { return nil } - b := strings.Builder{} - for _, msg := range msg.msgs { - b.WriteString(msg.TgString()) - b.WriteRune('\n') - } - message := b.String() + message := msg.Format(tpl) for _, chatId := range *chatIds { toSend := tgbotapi.NewMessage(chatId, message) toSend.ParseMode = "HTML" @@ -53,14 +49,6 @@ type RealSleeper struct { duration time.Duration } -type SatelMsgContent struct { - ev satel.Event -} - -func (self SatelMsgContent) TgString() string { - return fmt.Sprint("", self.ev.Type, ", index:", self.ev.Index, ", value:", self.ev.Value, "") -} - func (self RealSleeper) Sleep(ch chan<- interface{}) { go func() { time.Sleep(self.duration) @@ -146,10 +134,12 @@ func main() { tgSender := TgSender{bot} + tpl := template.Must(template.New("TelegramMessage").Parse(TelegramMessageTemplate)) + Consume( SendToTg( tgSenderWorker(tgEvents, &wg, sleeper, log.New(os.Stderr, "TgSender", log.Lmicroseconds)), - tgSender, &wg, log.New(os.Stderr, "SendToTg", log.Lmicroseconds))) + tgSender, &wg, log.New(os.Stderr, "SendToTg", log.Lmicroseconds), tpl)) go CloseSatelOnCtrlC(s) @@ -159,7 +149,7 @@ func main() { allowedIndexes) { logger.Print("Received change from SATEL: ", e) for _, chatId := range chatIds { - sendTgMessage(tgEvents, SatelMsgContent{e}, chatId) + sendTgMessage(tgEvents, MsgContent{e}, chatId) } } diff --git a/message_contents.go b/message_contents.go new file mode 100644 index 0000000..758b0ea --- /dev/null +++ b/message_contents.go @@ -0,0 +1,124 @@ +package main + +import ( + "fmt" + "html/template" + "strings" + + "github.com/probakowski/go-satel" +) + +type MsgContent struct { + SatelEvent satel.Event +} + +type GenericMessage struct { + ChatIds ChatId + Messages []MsgContent +} + +func (self GenericMessage) Format(template *template.Template) string { + b := strings.Builder{} + template.Execute(&b, self) + return b.String() +} + +func getEmojiWhenTrueIsGood(v bool) string { + if v { + return "✅" + } else { + return "🔴" + } +} + +func getEmojiWhenTrueIsBad(v bool) string { + if v { + return "🔴" + } else { + return "✅" + } +} + +func (self MsgContent) FormatEvent() string { + switch self.SatelEvent.Type { + case satel.ZoneViolation: + return fmt.Sprintf("%s: %s", self.SatelEvent.Type.String(), getEmojiWhenTrueIsBad(self.SatelEvent.Value)) + case satel.ZoneTamper: + return fmt.Sprintf("%s: %s", self.SatelEvent.Type.String(), getEmojiWhenTrueIsBad(self.SatelEvent.Value)) + case satel.ZoneAlarm: + return fmt.Sprintf("%s: %s", self.SatelEvent.Type.String(), getEmojiWhenTrueIsBad(self.SatelEvent.Value)) + case satel.ZoneTamperAlarm: + return fmt.Sprintf("%s: %s", self.SatelEvent.Type.String(), getEmojiWhenTrueIsBad(self.SatelEvent.Value)) + case satel.ZoneAlarmMemory: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.ZoneTamperAlarmMemory: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.ZoneBypass: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.ZoneNoViolationTrouble: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.ZoneLongViolationTrouble: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.ArmedPartitionSuppressed: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.ArmedPartition: + return fmt.Sprintf("%s: %s", self.SatelEvent.Type.String(), getEmojiWhenTrueIsGood(self.SatelEvent.Value)) + case satel.PartitionArmedInMode2: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.PartitionArmedInMode3: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.PartitionWith1stCodeEntered: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.PartitionEntryTime: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.PartitionExitTimeOver10s: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.PartitionExitTimeUnder10s: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.PartitionTemporaryBlocked: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.PartitionBlockedForGuardRound: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.PartitionAlarm: + return fmt.Sprintf("%s: %s", self.SatelEvent.Type.String(), getEmojiWhenTrueIsBad(self.SatelEvent.Value)) + case satel.PartitionFireAlarm: + return fmt.Sprintf("%s: %s", self.SatelEvent.Type.String(), getEmojiWhenTrueIsBad(self.SatelEvent.Value)) + case satel.PartitionAlarmMemory: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.PartitionFireAlarmMemory: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.Output: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.DoorOpened: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.DoorOpenedLong: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.StatusBit: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.TroublePart1: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.TroublePart2: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.TroublePart3: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.TroublePart4: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.TroublePart5: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.TroubleMemoryPart1: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.TroubleMemoryPart2: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.TroubleMemoryPart3: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.TroubleMemoryPart4: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.TroubleMemoryPart5: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.PartitionWithViolatedZones: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + case satel.ZoneIsolate: + return fmt.Sprintf("%s: %t", self.SatelEvent.Type.String(), self.SatelEvent.Value) + } + panic(fmt.Sprint("Unknown event received: ", self.SatelEvent)) +} diff --git a/sender_worker.go b/sender_worker.go index af41ce3..faafd64 100644 --- a/sender_worker.go +++ b/sender_worker.go @@ -1,12 +1,13 @@ package main import ( + "html/template" "log" "sync" ) type Sender interface { - Send(msg GenericMessage) error + Send(msg GenericMessage, tpl *template.Template) error } type Sleeper interface { @@ -32,22 +33,7 @@ func Consume(events <-chan GenericMessage) { }() } -type MsgContent interface { - TgString() string -} - -type StringMsgContent struct { - msg string -} - -func (self StringMsgContent) TgString() string { return self.msg } - -type GenericMessage struct { - chatIds ChatId - msgs []MsgContent -} - -func SendToTg(events <-chan GenericMessage, s Sender, wg *sync.WaitGroup, logger *log.Logger) <-chan GenericMessage { +func SendToTg(events <-chan GenericMessage, s Sender, wg *sync.WaitGroup, logger *log.Logger, tpl *template.Template) <-chan GenericMessage { returnEvents := make(chan GenericMessage) go func() { @@ -55,7 +41,7 @@ func SendToTg(events <-chan GenericMessage, s Sender, wg *sync.WaitGroup, logger defer wg.Done() for e := range events { returnEvents <- e - err := s.Send(e) + err := s.Send(e, tpl) if err != nil { // TODO: handle it better panic(err) @@ -86,11 +72,11 @@ func tgSenderWorker(tgEvents <-chan GenericMessage, wg *sync.WaitGroup, sleeper break loop } // Collect all messages to send them at once - _, messageBuilderExists := messagesToSend[ev.chatIds] + _, messageBuilderExists := messagesToSend[ev.ChatIds] if !messageBuilderExists { - messagesToSend[ev.chatIds] = make([]MsgContent, 0) + messagesToSend[ev.ChatIds] = make([]MsgContent, 0) } - messagesToSend[ev.chatIds] = append(messagesToSend[ev.chatIds], ev.msgs...) + messagesToSend[ev.ChatIds] = append(messagesToSend[ev.ChatIds], ev.Messages...) if !waitingStarted { logger.Print("Waiting for more messages to arrive before sending...") waitingStarted = true diff --git a/sender_worker_test.go b/sender_worker_test.go index c7922f1..784369a 100644 --- a/sender_worker_test.go +++ b/sender_worker_test.go @@ -1,11 +1,13 @@ package main import ( + "html/template" "io" "log" "sync" "testing" + "github.com/probakowski/go-satel" "github.com/stretchr/testify/assert" ) @@ -13,7 +15,7 @@ type MockSender struct { messages []GenericMessage } -func (self *MockSender) Send(msg GenericMessage) error { +func (self *MockSender) Send(msg GenericMessage, tpl *template.Template) error { self.messages = append(self.messages, msg) return nil } @@ -38,28 +40,37 @@ func (self FakeChatId) GetTgIds() *[]int64 { return nil } +var ( + messageTest1 = satel.Event{Type: satel.ArmedPartition, Index: 1, Value: true} + messageTest2 = satel.Event{Type: satel.ArmedPartition, Index: 2, Value: true} + messageTest3 = satel.Event{Type: satel.ArmedPartition, Index: 3, Value: true} + messageTest4 = satel.Event{Type: satel.ArmedPartition, Index: 4, Value: true} + messageTest5 = satel.Event{Type: satel.ArmedPartition, Index: 5, Value: true} + messageTest6 = satel.Event{Type: satel.ArmedPartition, Index: 6, Value: true} +) + func TestMessageThrottling(t *testing.T) { testEvents := make(chan GenericMessage) wg := sync.WaitGroup{} mockSender := MockSender{make([]GenericMessage, 0)} mockSleeper := MockSleeper{nil, 0} Consume(SendToTg(tgSenderWorker(testEvents, &wg, &mockSleeper, log.New(io.Discard, "", log.Ltime)), - &mockSender, &wg, log.New(io.Discard, "", log.Ltime))) - testEvents <- GenericMessage{TgChatId{123}, []MsgContent{StringMsgContent{"test1"}}} - testEvents <- GenericMessage{TgChatId{124}, []MsgContent{StringMsgContent{"test3"}}} - testEvents <- GenericMessage{TgChatId{123}, []MsgContent{StringMsgContent{"test2"}}} - testEvents <- GenericMessage{TgChatId{124}, []MsgContent{StringMsgContent{"test4"}}} - testEvents <- GenericMessage{FakeChatId{123}, []MsgContent{StringMsgContent{"testFake"}}} + &mockSender, &wg, log.New(io.Discard, "", log.Ltime), nil)) + testEvents <- GenericMessage{TgChatId{123}, []MsgContent{{messageTest1}}} + testEvents <- GenericMessage{TgChatId{124}, []MsgContent{{messageTest3}}} + testEvents <- GenericMessage{TgChatId{123}, []MsgContent{{messageTest2}}} + testEvents <- GenericMessage{TgChatId{124}, []MsgContent{{messageTest4}}} + testEvents <- GenericMessage{FakeChatId{123}, []MsgContent{{messageTest6}}} assert.Equal(t, 1, mockSleeper.callCount) *mockSleeper.ch <- nil assert.Equal(t, 1, mockSleeper.callCount) - testEvents <- GenericMessage{TgChatId{123}, []MsgContent{StringMsgContent{"test5"}}} + testEvents <- GenericMessage{TgChatId{123}, []MsgContent{{messageTest5}}} close(testEvents) wg.Wait() assert.Equal(t, 2, mockSleeper.callCount) assert.Len(t, mockSender.messages, 4) - assert.Contains(t, mockSender.messages, GenericMessage{TgChatId{123}, []MsgContent{StringMsgContent{"test1"}, StringMsgContent{"test2"}}}) - assert.Contains(t, mockSender.messages, GenericMessage{TgChatId{124}, []MsgContent{StringMsgContent{"test3"}, StringMsgContent{"test4"}}}) - assert.Contains(t, mockSender.messages, GenericMessage{FakeChatId{123}, []MsgContent{StringMsgContent{"testFake"}}}) + assert.Contains(t, mockSender.messages, GenericMessage{TgChatId{123}, []MsgContent{{messageTest1}, {messageTest2}}}) + assert.Contains(t, mockSender.messages, GenericMessage{TgChatId{124}, []MsgContent{{messageTest3}, {messageTest4}}}) + assert.Contains(t, mockSender.messages, GenericMessage{FakeChatId{123}, []MsgContent{{messageTest6}}}) } diff --git a/templates.go b/templates.go new file mode 100644 index 0000000..4109bab --- /dev/null +++ b/templates.go @@ -0,0 +1,8 @@ +package main + +const TelegramMessageTemplate = `Received following changes: +{{- range .Messages}} +:: {{.SatelEvent.Index}}: {{.FormatEvent}} +{{- else -}} +Huh, no messages - this is a bug +{{- end}}` diff --git a/templates_test.go b/templates_test.go new file mode 100644 index 0000000..bb7a7fe --- /dev/null +++ b/templates_test.go @@ -0,0 +1,40 @@ +package main + +import ( + "html/template" + "io" + "log" + "sync" + "testing" + + "github.com/probakowski/go-satel" + "github.com/stretchr/testify/assert" +) + +type MockTemplateSender struct { + message string +} + +func (self *MockTemplateSender) Send(msg GenericMessage, tpl *template.Template) error { + self.message = msg.Format(tpl) + return nil +} + +var ( + tplMessageTest1 = satel.Event{Type: satel.ArmedPartition, Index: 1, Value: true} + tplMessageTest2 = satel.Event{Type: satel.ZoneViolation, Index: 2, Value: true} +) + +func TestTelegramTemplate(t *testing.T) { + testEvents := make(chan GenericMessage) + wg := sync.WaitGroup{} + mockSender := MockTemplateSender{} + tpl, err := template.New("TestTemplate").Parse(TelegramMessageTemplate) + assert.NoError(t, err) + Consume(SendToTg(testEvents, &mockSender, &wg, log.New(io.Discard, "", log.Ltime), tpl)) + testEvents <- GenericMessage{TgChatId{123}, []MsgContent{{tplMessageTest1}, {tplMessageTest2}}} + close(testEvents) + wg.Wait() + + // assert.Equal(t, "siemka", mockSender.message) +}