1
0
Fork 0

Telegram message formatting

This commit is contained in:
Michał Rudowicz 2024-02-19 19:52:40 +01:00
parent 0e01644c3b
commit 795ffb66d9
6 changed files with 209 additions and 50 deletions

26
main.go
View File

@ -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("<b>", self.ev.Type, "</b>, <i>index</i>:<b>", self.ev.Index, "</b>, value:<b>", self.ev.Value, "</b>")
}
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)
}
}

124
message_contents.go Normal file
View File

@ -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))
}

View File

@ -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

View File

@ -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}}})
}

8
templates.go Normal file
View File

@ -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}}`

40
templates_test.go Normal file
View File

@ -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)
}