Skip to content

feat(telegram): send multiple messages when exceeding limits #251

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions pkg/format/formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ package format
import (
"errors"
"fmt"
"github.com/containrrr/shoutrrr/pkg/types"
"github.com/containrrr/shoutrrr/pkg/util"
r "reflect"
"strconv"
"strings"
"unsafe"

"github.com/containrrr/shoutrrr/pkg/types"
"github.com/containrrr/shoutrrr/pkg/util"
)

// GetServiceConfig returns the inner config of a service
Expand Down Expand Up @@ -139,7 +140,11 @@ func SetConfigField(config r.Value, field FieldInfo, inputValue string) (valid b
return false, errors.New("field format is not supported")
}

values := strings.Split(inputValue, ",")
values := []string{}
// an empty string should yield an empty slice
if len(inputValue) > 0 {
values = strings.Split(inputValue, ",")
}

var value r.Value
if elemKind == r.Struct {
Expand Down
8 changes: 8 additions & 0 deletions pkg/format/formatter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ var _ = Describe("SetConfigField", func() {
Expect(ts.StrSlice).To(HaveLen(2))
})
})
When("the value is empty", func() {
It("should be set to an empty slice ", func() {
valid, err := SetConfigField(tv, *nodeMap["StrSlice"].Field(), "")
Expect(valid).To(BeTrue())
Expect(err).NotTo(HaveOccurred())
Expect(ts.StrSlice).To(HaveLen(0))
})
})
})

When("setting a string array value", func() {
Expand Down
76 changes: 68 additions & 8 deletions pkg/services/telegram/telegram.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package telegram

import (
"errors"
"github.com/containrrr/shoutrrr/pkg/format"
"fmt"
"html"
"net/url"

"github.com/containrrr/shoutrrr/pkg/format"
"github.com/containrrr/shoutrrr/pkg/util"

"github.com/containrrr/shoutrrr/pkg/services/standard"
"github.com/containrrr/shoutrrr/pkg/types"
)
Expand All @@ -23,27 +26,84 @@ type Service struct {

// Send notification to Telegram
func (service *Service) Send(message string, params *types.Params) error {
if len(message) > maxlength {
return errors.New("Message exceeds the max length")
}

config := *service.config
if err := service.pkr.UpdateConfigFromParams(&config, params); err != nil {
return err
}

return service.sendMessageForChatIDs(message, &config)
msgs, omitted := splitMessages(&config, message)
if len(msgs) > 1 {
service.Logf("the message was split into %d messages", len(msgs))
}

var firstErr error
for _, msg := range msgs {
if err := service.sendMessageForChatIDs(msg, &config); err != nil {
service.Log(err)
if firstErr == nil {
firstErr = err
}
}
}

if omitted > 0 {
service.Logf("the message exceeded the total maximum amount of characters to send. %d characters were omitted", omitted)
}

if firstErr != nil {
return fmt.Errorf("failed to send telegram notification: %v", firstErr)
}

return nil
}

func splitMessages(config *Config, message string) (messages []string, omitted int) {
if config.ParseMode == ParseModes.None {
// no parse mode has been provided, treat message as unescaped HTML
message = html.EscapeString(message)
config.ParseMode = ParseModes.HTML
}

// Remove the HTML overhead and title length from the maximum message length
maxLen := maxlength - HTMLOverhead - len(config.Title)
messageLimits := types.MessageLimit{
ChunkSize: maxLen,
TotalChunkSize: maxLen * config.MaxSplitSends,
ChunkCount: config.MaxSplitSends,
}

items, omitted := util.PartitionMessage(message, messageLimits, 10)

title := config.Title

messages = make([]string, len(items))
for i, item := range items {
messages[i] = formatHTMLMessage(title, item.Text)
if i == 0 {
// Skip title for all but the first message
title = ""
}
}
return messages, omitted
}

// Initialize loads ServiceConfig from configURL and sets logger for this Service
func (service *Service) Initialize(configURL *url.URL, logger types.StdLogger) error {
service.Logger.SetLogger(logger)

service.config = &Config{
Preview: true,
Notification: true,
}
service.pkr = format.NewPropKeyResolver(service.config)
if err := service.config.setURL(&service.pkr, configURL); err != nil {
config := service.config

service.pkr = format.NewPropKeyResolver(config)
pkr := &service.pkr

_ = service.pkr.SetDefaultProps(config)

if err := config.setURL(pkr, configURL); err != nil {
return err
}

Expand Down
13 changes: 7 additions & 6 deletions pkg/services/telegram/telegram_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ import (

// Config for use within the telegram plugin
type Config struct {
Token string `url:"user"`
Preview bool `key:"preview" default:"Yes" desc:"If disabled, no web page preview will be displayed for URLs"`
Notification bool `key:"notification" default:"Yes" desc:"If disabled, sends Message silently"`
ParseMode parseMode `key:"parsemode" default:"None" desc:"How the text Message should be parsed"`
Chats []string `key:"chats,channels" desc:"Chat IDs or Channel names (using @channel-name)"`
Title string `key:"title" default:"" desc:"Notification title, optionally set by the sender"`
Token string `url:"user"`
Preview bool `key:"preview" default:"Yes" desc:"If disabled, no web page preview will be displayed for URLs"`
Notification bool `key:"notification" default:"Yes" desc:"If disabled, sends Message silently"`
ParseMode parseMode `key:"parsemode" default:"None" desc:"How the text Message should be parsed, removes template if set"`
Chats []string `key:"chats,channels" desc:"Chat IDs or Channel names (using @channel-name)"`
Title string `key:"title" default:"" desc:"Notification title, optionally set by the sender"`
MaxSplitSends int `key:"maxsends" default:"4" desc:"Maximum number of messages to send when the message exceeds the maximum length (4096)"`
}

// Enums returns the fields that should use a corresponding EnumFormatter to Print/Parse their values
Expand Down
15 changes: 8 additions & 7 deletions pkg/services/telegram/telegram_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ type Generator struct {
}

// Generate a telegram Shoutrrr configuration from a user dialog
func (g *Generator) Generate(_ types.Service, props map[string]string, _ []string) (types.ServiceConfig, error) {
var config Config
func (g *Generator) Generate(service types.Service, props map[string]string, _ []string) (types.ServiceConfig, error) {
config := Config{}
if g.Reader == nil {
g.Reader = os.Stdin
}
Expand Down Expand Up @@ -131,11 +131,12 @@ func (g *Generator) Generate(_ types.Service, props map[string]string, _ []strin

ud.Writeln("")

config = Config{
Notification: true,
Token: token,
Chats: g.chats,
}
pkr := f.NewPropKeyResolver(&config)
_ = pkr.SetDefaultProps(&config)

config.Notification = true
config.Token = token
config.Chats = g.chats

return &config, nil
}
Expand Down
9 changes: 5 additions & 4 deletions pkg/services/telegram/telegram_generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ package telegram_test

import (
"fmt"
"io"
"strings"

"github.com/jarcoal/httpmock"
"github.com/mattn/go-colorable"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"io"
"strings"

"github.com/containrrr/shoutrrr/pkg/services/telegram"
)
Expand Down Expand Up @@ -53,7 +54,7 @@ var _ = Describe("TelegramGenerator", func() {
AfterEach(func() {
httpmock.DeactivateAndReset()
})
It("should return the ", func() {
It("should return the expected URL from a session", func() {
gen := telegram.Generator{
Reader: userOut,
Writer: userInMono,
Expand Down Expand Up @@ -106,7 +107,7 @@ var _ = Describe("TelegramGenerator", func() {
Eventually(userIn).Should(gbytes.Say(`Selected chats:`))
Eventually(userIn).Should(gbytes.Say(`667 \(private\) @mockUser`))

Eventually(resultChannel).Should(Receive(Equal(`telegram://0:MockToken@telegram?chats=667&preview=No`)))
Eventually(resultChannel).Should(Receive(Equal(`telegram://0:MockToken@telegram?chats=667`)))
})

})
69 changes: 32 additions & 37 deletions pkg/services/telegram/telegram_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ package telegram
import (
"encoding/json"
"errors"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"log"
"net/url"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("the telegram service", func() {
Expand All @@ -18,15 +19,6 @@ var _ = Describe("the telegram service", func() {

Describe("creating configurations", func() {
When("given an url", func() {

When("a parse mode is not supplied", func() {
It("no parse_mode should be present in payload", func() {
payload, err := getPayloadStringFromURL("telegram://12345:mock-token@telegram/?channels=channel-1", "Message", logger)
Expect(err).NotTo(HaveOccurred())
Expect(payload).NotTo(ContainSubstring("parse_mode"))
})
})

When("a parse mode is supplied", func() {
When("it's set to a valid mode and not None", func() {
It("parse_mode should be present in payload", func() {
Expand All @@ -36,27 +28,18 @@ var _ = Describe("the telegram service", func() {
})
})
When("it's set to None", func() {
When("no title has been provided", func() {
It("no parse_mode should be present in payload", func() {
payload, err := getPayloadStringFromURL("telegram://12345:mock-token@telegram/?channels=channel-1&parsemode=None", "Message", logger)
Expect(err).NotTo(HaveOccurred())
Expect(payload).NotTo(ContainSubstring("parse_mode"))
})
payload, err := getSinglePayloadFromURL("telegram://12345:mock-token@telegram/?channels=channel-1&title=MessageTitle", `Oh wow! <3 Cool & stuff ->`, logger)
Expect(err).NotTo(HaveOccurred())
It("should have parse_mode set to HTML", func() {
Expect(payload.ParseMode).To(Equal("HTML"))
})
When("a title has been provided", func() {
payload, err := getPayloadFromURL("telegram://12345:mock-token@telegram/?channels=channel-1&title=MessageTitle", `Oh wow! <3 Cool & stuff ->`, logger)
Expect(err).NotTo(HaveOccurred())
It("should have parse_mode set to HTML", func() {
Expect(payload.ParseMode).To(Equal("HTML"))
})
It("should contain the title prepended in the message", func() {
Expect(payload.Text).To(ContainSubstring("MessageTitle"))
})
It("should escape the message HTML tags", func() {
Expect(payload.Text).To(ContainSubstring("&lt;3"))
Expect(payload.Text).To(ContainSubstring("Cool &amp; stuff"))
Expect(payload.Text).To(ContainSubstring("-&gt;"))
})
It("should contain the title prepended in the message", func() {
Expect(payload.Text).To(ContainSubstring("MessageTitle"))
})
It("should escape the message HTML tags", func() {
Expect(payload.Text).To(ContainSubstring("&lt;3"))
Expect(payload.Text).To(ContainSubstring("Cool &amp; stuff"))
Expect(payload.Text).To(ContainSubstring("-&gt;"))
})
})
})
Expand All @@ -65,28 +48,40 @@ var _ = Describe("the telegram service", func() {
})
})

func getPayloadFromURL(testURL string, message string, logger *log.Logger) (SendMessagePayload, error) {
func getPayloadsFromURL(testURL string, message string, logger *log.Logger) ([]SendMessagePayload, int, error) {
var payloads []SendMessagePayload
telegram := &Service{}

serviceURL, err := url.Parse(testURL)
if err != nil {
return SendMessagePayload{}, err
return payloads, 0, err
}

if err = telegram.Initialize(serviceURL, logger); err != nil {
return SendMessagePayload{}, err
return payloads, 0, err
}

if len(telegram.config.Chats) < 1 {
return SendMessagePayload{}, errors.New("no channels were supplied")
return payloads, 0, errors.New("no channels were supplied")
}

return createSendMessagePayload(message, telegram.config.Chats[0], telegram.config), nil
messages, omitted := splitMessages(telegram.config, message)

payloads = make([]SendMessagePayload, len(messages))
for i, msg := range messages {
payloads[i] = createSendMessagePayload(msg, telegram.config.Chats[0], telegram.config)
}
return payloads, omitted, nil

}

func getSinglePayloadFromURL(testURL string, message string, logger *log.Logger) (SendMessagePayload, error) {
payloads, _, err := getPayloadsFromURL(testURL, message, logger)
return payloads[0], err
}

func getPayloadStringFromURL(testURL string, message string, logger *log.Logger) ([]byte, error) {
payload, err := getPayloadFromURL(testURL, message, logger)
payload, err := getSinglePayloadFromURL(testURL, message, logger)
if err != nil {
return nil, err
}
Expand Down
Loading