Adding upstream version 0.8.9.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
3b2c48b5e4
commit
c0c4addb85
285 changed files with 25880 additions and 0 deletions
89
pkg/services/telegram/telegram.go
Normal file
89
pkg/services/telegram/telegram.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
package telegram
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/format"
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/services/standard"
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/types"
|
||||
)
|
||||
|
||||
// apiFormat defines the Telegram API endpoint template.
|
||||
const (
|
||||
apiFormat = "https://api.telegram.org/bot%s/%s"
|
||||
maxlength = 4096
|
||||
)
|
||||
|
||||
// ErrMessageTooLong indicates that the message exceeds the maximum allowed length.
|
||||
var (
|
||||
ErrMessageTooLong = errors.New("Message exceeds the max length")
|
||||
)
|
||||
|
||||
// Service sends notifications to configured Telegram chats.
|
||||
type Service struct {
|
||||
standard.Standard
|
||||
Config *Config
|
||||
pkr format.PropKeyResolver
|
||||
}
|
||||
|
||||
// Send delivers a notification message to Telegram.
|
||||
func (service *Service) Send(message string, params *types.Params) error {
|
||||
if len(message) > maxlength {
|
||||
return ErrMessageTooLong
|
||||
}
|
||||
|
||||
config := *service.Config
|
||||
if err := service.pkr.UpdateConfigFromParams(&config, params); err != nil {
|
||||
return fmt.Errorf("updating config from params: %w", err)
|
||||
}
|
||||
|
||||
return service.sendMessageForChatIDs(message, &config)
|
||||
}
|
||||
|
||||
// Initialize configures the service with a URL and logger.
|
||||
func (service *Service) Initialize(configURL *url.URL, logger types.StdLogger) error {
|
||||
service.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 {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetID returns the identifier for this service.
|
||||
func (service *Service) GetID() string {
|
||||
return Scheme
|
||||
}
|
||||
|
||||
// sendMessageForChatIDs sends the message to all configured chat IDs.
|
||||
func (service *Service) sendMessageForChatIDs(message string, config *Config) error {
|
||||
for _, chat := range service.Config.Chats {
|
||||
if err := sendMessageToAPI(message, chat, config); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetConfig returns the current configuration for the service.
|
||||
func (service *Service) GetConfig() *Config {
|
||||
return service.Config
|
||||
}
|
||||
|
||||
// sendMessageToAPI sends a message to the Telegram API for a specific chat.
|
||||
func sendMessageToAPI(message string, chat string, config *Config) error {
|
||||
client := &Client{token: config.Token}
|
||||
payload := createSendMessagePayload(message, chat, config)
|
||||
_, err := client.SendMessage(&payload)
|
||||
|
||||
return err
|
||||
}
|
74
pkg/services/telegram/telegram_client.go
Normal file
74
pkg/services/telegram/telegram_client.go
Normal file
|
@ -0,0 +1,74 @@
|
|||
package telegram
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/util/jsonclient"
|
||||
)
|
||||
|
||||
// Client for Telegram API.
|
||||
type Client struct {
|
||||
token string
|
||||
}
|
||||
|
||||
func (c *Client) apiURL(endpoint string) string {
|
||||
return fmt.Sprintf(apiFormat, c.token, endpoint)
|
||||
}
|
||||
|
||||
// GetBotInfo returns the bot User info.
|
||||
func (c *Client) GetBotInfo() (*User, error) {
|
||||
response := &userResponse{}
|
||||
err := jsonclient.Get(c.apiURL("getMe"), response)
|
||||
|
||||
if !response.OK {
|
||||
return nil, GetErrorResponse(jsonclient.ErrorBody(err))
|
||||
}
|
||||
|
||||
return &response.Result, nil
|
||||
}
|
||||
|
||||
// GetUpdates retrieves the latest updates.
|
||||
func (c *Client) GetUpdates(
|
||||
offset int,
|
||||
limit int,
|
||||
timeout int,
|
||||
allowedUpdates []string,
|
||||
) ([]Update, error) {
|
||||
request := &updatesRequest{
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Timeout: timeout,
|
||||
AllowedUpdates: allowedUpdates,
|
||||
}
|
||||
response := &updatesResponse{}
|
||||
err := jsonclient.Post(c.apiURL("getUpdates"), request, response)
|
||||
|
||||
if !response.OK {
|
||||
return nil, GetErrorResponse(jsonclient.ErrorBody(err))
|
||||
}
|
||||
|
||||
return response.Result, nil
|
||||
}
|
||||
|
||||
// SendMessage sends the specified Message.
|
||||
func (c *Client) SendMessage(message *SendMessagePayload) (*Message, error) {
|
||||
response := &messageResponse{}
|
||||
err := jsonclient.Post(c.apiURL("sendMessage"), message, response)
|
||||
|
||||
if !response.OK {
|
||||
return nil, GetErrorResponse(jsonclient.ErrorBody(err))
|
||||
}
|
||||
|
||||
return response.Result, nil
|
||||
}
|
||||
|
||||
// GetErrorResponse retrieves the error message from a failed request.
|
||||
func GetErrorResponse(body string) error {
|
||||
response := &responseError{}
|
||||
if err := json.Unmarshal([]byte(body), response); err == nil {
|
||||
return response
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
94
pkg/services/telegram/telegram_config.go
Normal file
94
pkg/services/telegram/telegram_config.go
Normal file
|
@ -0,0 +1,94 @@
|
|||
package telegram
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/format"
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/types"
|
||||
)
|
||||
|
||||
// Scheme identifies this service in configuration URLs.
|
||||
const (
|
||||
Scheme = "telegram"
|
||||
)
|
||||
|
||||
// ErrInvalidToken indicates an invalid Telegram token format or content.
|
||||
var (
|
||||
ErrInvalidToken = errors.New("invalid telegram token")
|
||||
ErrNoChannelsDefined = errors.New("no channels defined in config URL")
|
||||
)
|
||||
|
||||
// Config holds settings for the Telegram notification service.
|
||||
type Config struct {
|
||||
Token string `url:"user"`
|
||||
Preview bool ` default:"Yes" desc:"If disabled, no web page preview will be displayed for URLs" key:"preview"`
|
||||
Notification bool ` default:"Yes" desc:"If disabled, sends Message silently" key:"notification"`
|
||||
ParseMode parseMode ` default:"None" desc:"How the text Message should be parsed" key:"parsemode"`
|
||||
Chats []string ` desc:"Chat IDs or Channel names (using @channel-name)" key:"chats,channels"`
|
||||
Title string ` default:"" desc:"Notification title, optionally set by the sender" key:"title"`
|
||||
}
|
||||
|
||||
// Enums returns the fields that use an EnumFormatter for their values.
|
||||
func (config *Config) Enums() map[string]types.EnumFormatter {
|
||||
return map[string]types.EnumFormatter{
|
||||
"ParseMode": ParseModes.Enum,
|
||||
}
|
||||
}
|
||||
|
||||
// GetURL generates a URL from the current configuration values.
|
||||
func (config *Config) GetURL() *url.URL {
|
||||
resolver := format.NewPropKeyResolver(config)
|
||||
|
||||
return config.getURL(&resolver)
|
||||
}
|
||||
|
||||
// SetURL updates the configuration from a URL representation.
|
||||
func (config *Config) SetURL(url *url.URL) error {
|
||||
resolver := format.NewPropKeyResolver(config)
|
||||
|
||||
return config.setURL(&resolver, url)
|
||||
}
|
||||
|
||||
// getURL constructs a URL from the Config's fields using the provided resolver.
|
||||
func (config *Config) getURL(resolver types.ConfigQueryResolver) *url.URL {
|
||||
tokenParts := strings.Split(config.Token, ":")
|
||||
|
||||
return &url.URL{
|
||||
User: url.UserPassword(tokenParts[0], tokenParts[1]),
|
||||
Host: Scheme,
|
||||
Scheme: Scheme,
|
||||
ForceQuery: true,
|
||||
RawQuery: format.BuildQuery(resolver),
|
||||
}
|
||||
}
|
||||
|
||||
// setURL updates the Config from a URL using the provided resolver.
|
||||
func (config *Config) setURL(resolver types.ConfigQueryResolver, url *url.URL) error {
|
||||
password, _ := url.User.Password()
|
||||
|
||||
token := url.User.Username() + ":" + password
|
||||
if url.String() != "telegram://dummy@dummy.com" {
|
||||
if !IsTokenValid(token) {
|
||||
return fmt.Errorf("%w: %s", ErrInvalidToken, token)
|
||||
}
|
||||
}
|
||||
|
||||
for key, vals := range url.Query() {
|
||||
if err := resolver.Set(key, vals[0]); err != nil {
|
||||
return fmt.Errorf("setting config property %q from URL query: %w", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
if url.String() != "telegram://dummy@dummy.com" {
|
||||
if len(config.Chats) < 1 {
|
||||
return ErrNoChannelsDefined
|
||||
}
|
||||
}
|
||||
|
||||
config.Token = token
|
||||
|
||||
return nil
|
||||
}
|
215
pkg/services/telegram/telegram_generator.go
Normal file
215
pkg/services/telegram/telegram_generator.go
Normal file
|
@ -0,0 +1,215 @@
|
|||
package telegram
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"slices"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/format"
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/types"
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/util/generator"
|
||||
)
|
||||
|
||||
// UpdatesLimit defines the number of updates to retrieve per API call.
|
||||
const (
|
||||
UpdatesLimit = 10 // Number of updates to retrieve per call
|
||||
UpdatesTimeout = 120 // Timeout in seconds for long polling
|
||||
)
|
||||
|
||||
// ErrNoChatsSelected indicates that no chats were selected during generation.
|
||||
var (
|
||||
ErrNoChatsSelected = errors.New("no chats were selected")
|
||||
)
|
||||
|
||||
// Generator facilitates Telegram-specific URL generation via user interaction.
|
||||
type Generator struct {
|
||||
userDialog *generator.UserDialog
|
||||
client *Client
|
||||
chats []string
|
||||
chatNames []string
|
||||
chatTypes []string
|
||||
done bool
|
||||
botName string
|
||||
Reader io.Reader
|
||||
Writer io.Writer
|
||||
}
|
||||
|
||||
// Generate creates a Telegram Shoutrrr configuration from user dialog input.
|
||||
func (g *Generator) Generate(
|
||||
_ types.Service,
|
||||
props map[string]string,
|
||||
_ []string,
|
||||
) (types.ServiceConfig, error) {
|
||||
var config Config
|
||||
|
||||
if g.Reader == nil {
|
||||
g.Reader = os.Stdin
|
||||
}
|
||||
|
||||
if g.Writer == nil {
|
||||
g.Writer = os.Stdout
|
||||
}
|
||||
|
||||
g.userDialog = generator.NewUserDialog(g.Reader, g.Writer, props)
|
||||
userDialog := g.userDialog
|
||||
|
||||
userDialog.Writelnf(
|
||||
"To start we need your bot token. If you haven't created a bot yet, you can use this link:",
|
||||
)
|
||||
userDialog.Writelnf(" %v", format.ColorizeLink("https://t.me/botfather?start"))
|
||||
userDialog.Writelnf("")
|
||||
|
||||
token := userDialog.QueryString(
|
||||
"Enter your bot token:",
|
||||
generator.ValidateFormat(IsTokenValid),
|
||||
"token",
|
||||
)
|
||||
|
||||
userDialog.Writelnf("Fetching bot info...")
|
||||
|
||||
g.client = &Client{token: token}
|
||||
|
||||
botInfo, err := g.client.GetBotInfo()
|
||||
if err != nil {
|
||||
return &Config{}, err
|
||||
}
|
||||
|
||||
g.botName = botInfo.Username
|
||||
|
||||
userDialog.Writelnf("")
|
||||
userDialog.Writelnf(
|
||||
"Okay! %v will listen for any messages in PMs and group chats it is invited to.",
|
||||
format.ColorizeString("@", g.botName),
|
||||
)
|
||||
|
||||
g.done = false
|
||||
lastUpdate := 0
|
||||
|
||||
signals := make(chan os.Signal, 1)
|
||||
|
||||
// Subscribe to system signals
|
||||
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
for !g.done {
|
||||
userDialog.Writelnf("Waiting for messages to arrive...")
|
||||
|
||||
updates, err := g.client.GetUpdates(lastUpdate, UpdatesLimit, UpdatesTimeout, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// If no updates were retrieved, prompt user to continue
|
||||
promptDone := len(updates) == 0
|
||||
|
||||
for _, update := range updates {
|
||||
lastUpdate = update.UpdateID + 1
|
||||
|
||||
switch {
|
||||
case update.Message != nil || update.ChannelPost != nil:
|
||||
message := update.Message
|
||||
if update.ChannelPost != nil {
|
||||
message = update.ChannelPost
|
||||
}
|
||||
|
||||
chat := message.Chat
|
||||
|
||||
source := message.Chat.Name()
|
||||
if message.From != nil {
|
||||
source = "@" + message.From.Username
|
||||
}
|
||||
|
||||
userDialog.Writelnf("Got Message '%v' from %v in %v chat %v",
|
||||
format.ColorizeString(message.Text),
|
||||
format.ColorizeProp(source),
|
||||
format.ColorizeEnum(chat.Type),
|
||||
format.ColorizeNumber(chat.ID))
|
||||
userDialog.Writelnf(g.addChat(chat))
|
||||
// Another chat was added, prompt user to continue
|
||||
promptDone = true
|
||||
|
||||
case update.ChatMemberUpdate != nil:
|
||||
cmu := update.ChatMemberUpdate
|
||||
oldStatus := cmu.OldChatMember.Status
|
||||
newStatus := cmu.NewChatMember.Status
|
||||
userDialog.Writelnf(
|
||||
"Got a bot chat member update for %v, status was changed from %v to %v",
|
||||
format.ColorizeProp(cmu.Chat.Name()),
|
||||
format.ColorizeEnum(oldStatus),
|
||||
format.ColorizeEnum(newStatus),
|
||||
)
|
||||
|
||||
default:
|
||||
userDialog.Writelnf("Got unknown Update. Ignored!")
|
||||
}
|
||||
}
|
||||
|
||||
if promptDone {
|
||||
userDialog.Writelnf("")
|
||||
|
||||
g.done = !userDialog.QueryBool(
|
||||
fmt.Sprintf("Got %v chat ID(s) so far. Want to add some more?",
|
||||
format.ColorizeNumber(len(g.chats))),
|
||||
"",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
userDialog.Writelnf("")
|
||||
userDialog.Writelnf("Cleaning up the bot session...")
|
||||
|
||||
// Notify API that we got the updates
|
||||
if _, err = g.client.GetUpdates(lastUpdate, 0, 0, nil); err != nil {
|
||||
g.userDialog.Writelnf(
|
||||
"Failed to mark last updates as received: %v",
|
||||
format.ColorizeError(err),
|
||||
)
|
||||
}
|
||||
|
||||
if len(g.chats) < 1 {
|
||||
return nil, ErrNoChatsSelected
|
||||
}
|
||||
|
||||
userDialog.Writelnf("Selected chats:")
|
||||
|
||||
for i, id := range g.chats {
|
||||
name := g.chatNames[i]
|
||||
chatType := g.chatTypes[i]
|
||||
userDialog.Writelnf(
|
||||
" %v (%v) %v",
|
||||
format.ColorizeNumber(id),
|
||||
format.ColorizeEnum(chatType),
|
||||
format.ColorizeString(name),
|
||||
)
|
||||
}
|
||||
|
||||
userDialog.Writelnf("")
|
||||
|
||||
config = Config{
|
||||
Notification: true,
|
||||
Token: token,
|
||||
Chats: g.chats,
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// addChat adds a chat to the generator's list if it’s not already present.
|
||||
func (g *Generator) addChat(chat *Chat) string {
|
||||
chatID := strconv.FormatInt(chat.ID, 10)
|
||||
name := chat.Name()
|
||||
|
||||
if slices.Contains(g.chats, chatID) {
|
||||
return fmt.Sprintf("chat %v is already selected!", format.ColorizeString(name))
|
||||
}
|
||||
|
||||
g.chats = append(g.chats, chatID)
|
||||
g.chatNames = append(g.chatNames, name)
|
||||
g.chatTypes = append(g.chatTypes, chat.Type)
|
||||
|
||||
return fmt.Sprintf("Added new chat %v!", format.ColorizeString(name))
|
||||
}
|
131
pkg/services/telegram/telegram_generator_test.go
Normal file
131
pkg/services/telegram/telegram_generator_test.go
Normal file
|
@ -0,0 +1,131 @@
|
|||
package telegram_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/jarcoal/httpmock"
|
||||
"github.com/mattn/go-colorable"
|
||||
"github.com/onsi/ginkgo/v2"
|
||||
"github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/services/telegram"
|
||||
)
|
||||
|
||||
const (
|
||||
mockToken = `0:MockToken`
|
||||
mockAPIBase = "https://api.telegram.org/bot" + mockToken + "/"
|
||||
)
|
||||
|
||||
var (
|
||||
userOut *gbytes.Buffer
|
||||
userIn *gbytes.Buffer
|
||||
userInMono io.Writer
|
||||
)
|
||||
|
||||
func mockTyped(a ...any) {
|
||||
fmt.Fprint(userOut, a...)
|
||||
fmt.Fprint(userOut, "\n")
|
||||
}
|
||||
|
||||
func dumpBuffers() {
|
||||
for _, line := range strings.Split(string(userIn.Contents()), "\n") {
|
||||
fmt.Fprint(ginkgo.GinkgoWriter, "> ", line, "\n")
|
||||
}
|
||||
|
||||
for _, line := range strings.Split(string(userOut.Contents()), "\n") {
|
||||
fmt.Fprint(ginkgo.GinkgoWriter, "< ", line, "\n")
|
||||
}
|
||||
}
|
||||
|
||||
func mockAPI(endpoint string) string {
|
||||
return mockAPIBase + endpoint
|
||||
}
|
||||
|
||||
var _ = ginkgo.Describe("TelegramGenerator", func() {
|
||||
ginkgo.BeforeEach(func() {
|
||||
userOut = gbytes.NewBuffer()
|
||||
userIn = gbytes.NewBuffer()
|
||||
userInMono = colorable.NewNonColorable(userIn)
|
||||
httpmock.Activate()
|
||||
})
|
||||
ginkgo.AfterEach(func() {
|
||||
httpmock.DeactivateAndReset()
|
||||
})
|
||||
ginkgo.It("should return the ", func() {
|
||||
gen := telegram.Generator{
|
||||
Reader: userOut,
|
||||
Writer: userInMono,
|
||||
}
|
||||
|
||||
resultChannel := make(chan string, 1)
|
||||
|
||||
httpmock.RegisterResponder(
|
||||
"GET",
|
||||
mockAPI(`getMe`),
|
||||
httpmock.NewJsonResponderOrPanic(200, &struct {
|
||||
OK bool
|
||||
Result *telegram.User
|
||||
}{
|
||||
true, &telegram.User{
|
||||
ID: 1,
|
||||
IsBot: true,
|
||||
Username: "mockbot",
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
httpmock.RegisterResponder(
|
||||
"POST",
|
||||
mockAPI(`getUpdates`),
|
||||
httpmock.NewJsonResponderOrPanic(200, &struct {
|
||||
OK bool
|
||||
Result []telegram.Update
|
||||
}{
|
||||
true,
|
||||
[]telegram.Update{
|
||||
{
|
||||
ChatMemberUpdate: &telegram.ChatMemberUpdate{
|
||||
Chat: &telegram.Chat{Type: `channel`, Title: `mockChannel`},
|
||||
OldChatMember: &telegram.ChatMember{Status: `kicked`},
|
||||
NewChatMember: &telegram.ChatMember{Status: `administrator`},
|
||||
},
|
||||
},
|
||||
{
|
||||
Message: &telegram.Message{
|
||||
Text: "hi!",
|
||||
From: &telegram.User{Username: `mockUser`},
|
||||
Chat: &telegram.Chat{Type: `private`, ID: 667, Username: `mockUser`},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
go func() {
|
||||
defer ginkgo.GinkgoRecover()
|
||||
conf, err := gen.Generate(nil, nil, nil)
|
||||
|
||||
gomega.Expect(conf).ToNot(gomega.BeNil())
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
resultChannel <- conf.GetURL().String()
|
||||
}()
|
||||
|
||||
defer dumpBuffers()
|
||||
|
||||
mockTyped(mockToken)
|
||||
mockTyped(`no`)
|
||||
|
||||
gomega.Eventually(userIn).
|
||||
Should(gbytes.Say(`Got a bot chat member update for mockChannel, status was changed from kicked to administrator`))
|
||||
gomega.Eventually(userIn).
|
||||
Should(gbytes.Say(`Got 1 chat ID\(s\) so far\. Want to add some more\?`))
|
||||
gomega.Eventually(userIn).Should(gbytes.Say(`Selected chats:`))
|
||||
gomega.Eventually(userIn).Should(gbytes.Say(`667 \(private\) @mockUser`))
|
||||
|
||||
gomega.Eventually(resultChannel).
|
||||
Should(gomega.Receive(gomega.Equal(`telegram://0:MockToken@telegram?chats=667&preview=No`)))
|
||||
})
|
||||
})
|
153
pkg/services/telegram/telegram_internal_test.go
Normal file
153
pkg/services/telegram/telegram_internal_test.go
Normal file
|
@ -0,0 +1,153 @@
|
|||
package telegram
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/url"
|
||||
|
||||
"github.com/onsi/ginkgo/v2"
|
||||
"github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gstruct"
|
||||
)
|
||||
|
||||
var _ = ginkgo.Describe("the telegram service", func() {
|
||||
var logger *log.Logger
|
||||
|
||||
ginkgo.BeforeEach(func() {
|
||||
logger = log.New(ginkgo.GinkgoWriter, "Test", log.LstdFlags)
|
||||
})
|
||||
|
||||
ginkgo.Describe("creating configurations", func() {
|
||||
ginkgo.When("given an url", func() {
|
||||
ginkgo.When("a parse mode is not supplied", func() {
|
||||
ginkgo.It("no parse_mode should be present in payload", func() {
|
||||
payload, err := getPayloadStringFromURL(
|
||||
"telegram://12345:mock-token@telegram/?channels=channel-1",
|
||||
"Message",
|
||||
logger,
|
||||
)
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
gomega.Expect(payload).NotTo(gomega.ContainSubstring("parse_mode"))
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.When("a parse mode is supplied", func() {
|
||||
ginkgo.When("it's set to a valid mode and not None", func() {
|
||||
ginkgo.It("parse_mode should be present in payload", func() {
|
||||
payload, err := getPayloadStringFromURL(
|
||||
"telegram://12345:mock-token@telegram/?channels=channel-1&parsemode=Markdown",
|
||||
"Message",
|
||||
logger,
|
||||
)
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
gomega.Expect(payload).To(gomega.ContainSubstring("parse_mode"))
|
||||
})
|
||||
})
|
||||
ginkgo.When("it's set to None", func() {
|
||||
ginkgo.When("no title has been provided", func() {
|
||||
ginkgo.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,
|
||||
)
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
gomega.Expect(payload).NotTo(gomega.ContainSubstring("parse_mode"))
|
||||
})
|
||||
})
|
||||
ginkgo.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,
|
||||
)
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
ginkgo.It("should have parse_mode set to HTML", func() {
|
||||
gomega.Expect(payload.ParseMode).To(gomega.Equal("HTML"))
|
||||
})
|
||||
ginkgo.It("should contain the title prepended in the message", func() {
|
||||
gomega.Expect(payload.Text).To(gomega.ContainSubstring("MessageTitle"))
|
||||
})
|
||||
ginkgo.It("should escape the message HTML tags", func() {
|
||||
gomega.Expect(payload.Text).To(gomega.ContainSubstring("<3"))
|
||||
gomega.Expect(payload.Text).
|
||||
To(gomega.ContainSubstring("Cool & stuff"))
|
||||
gomega.Expect(payload.Text).To(gomega.ContainSubstring("->"))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.When("parsing URL that might have a message thread id", func() {
|
||||
ginkgo.When("no thread id is provided", func() {
|
||||
payload, err := getPayloadFromURL(
|
||||
"telegram://12345:mock-token@telegram/?channels=channel-1&title=MessageTitle",
|
||||
`Oh wow! <3 Cool & stuff ->`,
|
||||
logger,
|
||||
)
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
ginkgo.It("should have message_thread_id set to nil", func() {
|
||||
gomega.Expect(payload.MessageThreadID).To(gomega.BeNil())
|
||||
})
|
||||
})
|
||||
ginkgo.When("a numeric thread id is provided", func() {
|
||||
payload, err := getPayloadFromURL(
|
||||
"telegram://12345:mock-token@telegram/?channels=channel-1:10&title=MessageTitle",
|
||||
`Oh wow! <3 Cool & stuff ->`,
|
||||
logger,
|
||||
)
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
ginkgo.It("should have message_thread_id set to 10", func() {
|
||||
gomega.Expect(payload.MessageThreadID).To(gstruct.PointTo(gomega.Equal(10)))
|
||||
})
|
||||
})
|
||||
ginkgo.When("non-numeric thread id is provided", func() {
|
||||
payload, err := getPayloadFromURL(
|
||||
"telegram://12345:mock-token@telegram/?channels=channel-1:invalid&title=MessageTitle",
|
||||
`Oh wow! <3 Cool & stuff ->`,
|
||||
logger,
|
||||
)
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
ginkgo.It("should have message_thread_id set to nil", func() {
|
||||
gomega.Expect(payload.MessageThreadID).To(gomega.BeNil())
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
func getPayloadFromURL(
|
||||
testURL string,
|
||||
message string,
|
||||
logger *log.Logger,
|
||||
) (SendMessagePayload, error) {
|
||||
telegram := &Service{}
|
||||
|
||||
serviceURL, err := url.Parse(testURL)
|
||||
if err != nil {
|
||||
return SendMessagePayload{}, err
|
||||
}
|
||||
|
||||
if err = telegram.Initialize(serviceURL, logger); err != nil {
|
||||
return SendMessagePayload{}, err
|
||||
}
|
||||
|
||||
if len(telegram.Config.Chats) < 1 {
|
||||
return SendMessagePayload{}, errors.New("no channels were supplied")
|
||||
}
|
||||
|
||||
return createSendMessagePayload(message, telegram.Config.Chats[0], telegram.Config), nil
|
||||
}
|
||||
|
||||
func getPayloadStringFromURL(testURL string, message string, logger *log.Logger) ([]byte, error) {
|
||||
payload, err := getPayloadFromURL(testURL, message, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
raw, err := json.Marshal(payload)
|
||||
|
||||
return raw, err
|
||||
}
|
234
pkg/services/telegram/telegram_json.go
Normal file
234
pkg/services/telegram/telegram_json.go
Normal file
|
@ -0,0 +1,234 @@
|
|||
package telegram
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SendMessagePayload is the notification payload for the telegram notification service.
|
||||
type SendMessagePayload struct {
|
||||
Text string `json:"text"`
|
||||
ID string `json:"chat_id"`
|
||||
MessageThreadID *int `json:"message_thread_id,omitempty"`
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
DisablePreview bool `json:"disable_web_page_preview"`
|
||||
DisableNotification bool `json:"disable_notification"`
|
||||
ReplyMarkup *replyMarkup `json:"reply_markup,omitempty"`
|
||||
Entities []entity `json:"entities,omitempty"`
|
||||
ReplyTo int64 `json:"reply_to_message_id"`
|
||||
MessageID int64 `json:"message_id,omitempty"`
|
||||
}
|
||||
|
||||
// Message represents one chat message.
|
||||
type Message struct {
|
||||
MessageID int64 `json:"message_id"`
|
||||
Text string `json:"text"`
|
||||
From *User `json:"from"`
|
||||
Chat *Chat `json:"chat"`
|
||||
}
|
||||
|
||||
type messageResponse struct {
|
||||
OK bool `json:"ok"`
|
||||
Result *Message `json:"result"`
|
||||
}
|
||||
|
||||
type responseError struct {
|
||||
OK bool `json:"ok"`
|
||||
ErrorCode int `json:"error_code"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type userResponse struct {
|
||||
OK bool `json:"ok"`
|
||||
Result User `json:"result"`
|
||||
}
|
||||
|
||||
// User contains information about a telegram user or bot.
|
||||
type User struct {
|
||||
// Unique identifier for this User or bot
|
||||
ID int64 `json:"id"`
|
||||
// True, if this User is a bot
|
||||
IsBot bool `json:"is_bot"`
|
||||
// User's or bot's first name
|
||||
FirstName string `json:"first_name"`
|
||||
// Optional. User's or bot's last name
|
||||
LastName string `json:"last_name"`
|
||||
// Optional. User's or bot's username
|
||||
Username string `json:"username"`
|
||||
// Optional. IETF language tag of the User's language
|
||||
LanguageCode string `json:"language_code"`
|
||||
// Optional. True, if the bot can be invited to groups. Returned only in getMe.
|
||||
CanJoinGroups bool `json:"can_join_groups"`
|
||||
// Optional. True, if privacy mode is disabled for the bot. Returned only in getMe.
|
||||
CanReadAllGroupMessages bool `json:"can_read_all_group_messages"`
|
||||
// Optional. True, if the bot supports inline queries. Returned only in getMe.
|
||||
SupportsInlineQueries bool `json:"supports_inline_queries"`
|
||||
}
|
||||
|
||||
type updatesRequest struct {
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit"`
|
||||
Timeout int `json:"timeout"`
|
||||
AllowedUpdates []string `json:"allowed_updates"`
|
||||
}
|
||||
|
||||
type updatesResponse struct {
|
||||
OK bool `json:"ok"`
|
||||
Result []Update `json:"result"`
|
||||
}
|
||||
|
||||
type inlineQuery struct {
|
||||
// Unique identifier for this query
|
||||
ID string `json:"id"`
|
||||
// Sender
|
||||
From User `json:"from"`
|
||||
// Text of the query (up to 256 characters)
|
||||
Query string `json:"query"`
|
||||
// Offset of the results to be returned, can be controlled by the bot
|
||||
Offset string `json:"offset"`
|
||||
}
|
||||
|
||||
type chosenInlineResult struct{}
|
||||
|
||||
// Update contains state changes since the previous Update.
|
||||
type Update struct {
|
||||
// The Update's unique identifier. Update identifiers start from a certain positive number and increase sequentially. This ID becomes especially handy if you're using Webhooks, since it allows you to ignore repeated updates or to restore the correct Update sequence, should they get out of order. If there are no new updates for at least a week, then identifier of the next Update will be chosen randomly instead of sequentially.
|
||||
UpdateID int `json:"update_id"`
|
||||
// Optional. New incoming Message of any kind — text, photo, sticker, etc.
|
||||
Message *Message `json:"Message"`
|
||||
// Optional. New version of a Message that is known to the bot and was edited
|
||||
EditedMessage *Message `json:"edited_message"`
|
||||
// Optional. New incoming channel post of any kind — text, photo, sticker, etc.
|
||||
ChannelPost *Message `json:"channel_post"`
|
||||
// Optional. New version of a channel post that is known to the bot and was edited
|
||||
EditedChannelPost *Message `json:"edited_channel_post"`
|
||||
// Optional. New incoming inline query
|
||||
InlineQuery *inlineQuery `json:"inline_query"`
|
||||
//// Optional. The result of an inline query that was chosen by a User and sent to their chat partner. Please see our documentation on the feedback collecting for details on how to enable these updates for your bot.
|
||||
ChosenInlineResult *chosenInlineResult `json:"chosen_inline_result"`
|
||||
//// Optional. New incoming callback query
|
||||
CallbackQuery *callbackQuery `json:"callback_query"`
|
||||
|
||||
// API fields that are not used by the client has been commented out
|
||||
|
||||
//// Optional. New incoming shipping query. Only for invoices with flexible price
|
||||
// ShippingQuery `json:"shipping_query"`
|
||||
//// Optional. New incoming pre-checkout query. Contains full information about checkout
|
||||
// PreCheckoutQuery `json:"pre_checkout_query"`
|
||||
//// Optional. New poll state. Bots receive only updates about stopped polls and polls, which are sent by the bot
|
||||
// Poll `json:"poll"`
|
||||
//// Optional. A User changed their answer in a non-anonymous poll. Bots receive new votes only in polls that were sent by the bot itself.
|
||||
// Poll_answer PollAnswer `json:"poll_answer"`
|
||||
|
||||
ChatMemberUpdate *ChatMemberUpdate `json:"my_chat_member"`
|
||||
}
|
||||
|
||||
// Chat represents a telegram conversation.
|
||||
type Chat struct {
|
||||
ID int64 `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type inlineKey struct {
|
||||
Text string `json:"text"`
|
||||
URL string `json:"url"`
|
||||
LoginURL string `json:"login_url"`
|
||||
CallbackData string `json:"callback_data"`
|
||||
SwitchInlineQuery string `json:"switch_inline_query"`
|
||||
SwitchInlineQueryCurrent string `json:"switch_inline_query_current_chat"`
|
||||
}
|
||||
|
||||
type replyMarkup struct {
|
||||
InlineKeyboard [][]inlineKey `json:"inline_keyboard,omitempty"`
|
||||
}
|
||||
|
||||
type entity struct {
|
||||
Type string `json:"type"`
|
||||
Offset int `json:"offset"`
|
||||
Length int `json:"length"`
|
||||
}
|
||||
|
||||
type callbackQuery struct {
|
||||
ID string `json:"id"`
|
||||
From *User `json:"from"`
|
||||
Message *Message `json:"Message"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
// ChatMemberUpdate represents a member update in a telegram chat.
|
||||
type ChatMemberUpdate struct {
|
||||
// Chat the user belongs to
|
||||
Chat *Chat `json:"chat"`
|
||||
// Performer of the action, which resulted in the change
|
||||
From *User `json:"from"`
|
||||
// Date the change was done in Unix time
|
||||
Date int `json:"date"`
|
||||
// Previous information about the chat member
|
||||
OldChatMember *ChatMember `json:"old_chat_member"`
|
||||
// New information about the chat member
|
||||
NewChatMember *ChatMember `json:"new_chat_member"`
|
||||
// Optional. Chat invite link, which was used by the user to join the chat; for joining by invite link events only.
|
||||
// invite_link ChatInviteLink
|
||||
}
|
||||
|
||||
// ChatMember represents the membership state for a user in a telegram chat.
|
||||
type ChatMember struct {
|
||||
// The member's status in the chat
|
||||
Status string `json:"status"`
|
||||
// Information about the user
|
||||
User *User `json:"user"`
|
||||
}
|
||||
|
||||
func (e *responseError) Error() string {
|
||||
return e.Description
|
||||
}
|
||||
|
||||
// Name returns the name of the channel based on its type.
|
||||
func (c *Chat) Name() string {
|
||||
if c.Type == "private" || c.Type == "channel" && c.Username != "" {
|
||||
return "@" + c.Username
|
||||
}
|
||||
|
||||
return c.Title
|
||||
}
|
||||
|
||||
func createSendMessagePayload(message string, channel string, config *Config) SendMessagePayload {
|
||||
var threadID *int
|
||||
|
||||
chatID, thread, ok := strings.Cut(channel, ":")
|
||||
if ok {
|
||||
if parsed, err := strconv.Atoi(thread); err == nil {
|
||||
threadID = &parsed
|
||||
}
|
||||
}
|
||||
|
||||
payload := SendMessagePayload{
|
||||
Text: message,
|
||||
ID: chatID,
|
||||
MessageThreadID: threadID,
|
||||
DisableNotification: !config.Notification,
|
||||
DisablePreview: !config.Preview,
|
||||
}
|
||||
|
||||
parseMode := config.ParseMode
|
||||
if config.ParseMode == ParseModes.None && config.Title != "" {
|
||||
parseMode = ParseModes.HTML
|
||||
// no parse mode has been provided, treat message as unescaped HTML
|
||||
message = html.EscapeString(message)
|
||||
}
|
||||
|
||||
if parseMode != ParseModes.None {
|
||||
payload.ParseMode = parseMode.String()
|
||||
}
|
||||
|
||||
// only HTML parse mode is supported for titles
|
||||
if parseMode == ParseModes.HTML {
|
||||
payload.Text = fmt.Sprintf("<b>%v</b>\n%v", html.EscapeString(config.Title), message)
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
42
pkg/services/telegram/telegram_parsemode.go
Normal file
42
pkg/services/telegram/telegram_parsemode.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
package telegram
|
||||
|
||||
import (
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/format"
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/types"
|
||||
)
|
||||
|
||||
const (
|
||||
ParseModeNone parseMode = iota // 0
|
||||
ParseModeMarkdown // 1
|
||||
ParseModeHTML // 2
|
||||
ParseModeMarkdownV2 // 3
|
||||
)
|
||||
|
||||
// ParseModes is an enum helper for parseMode.
|
||||
var ParseModes = &parseModeVals{
|
||||
None: ParseModeNone,
|
||||
Markdown: ParseModeMarkdown,
|
||||
HTML: ParseModeHTML,
|
||||
MarkdownV2: ParseModeMarkdownV2,
|
||||
Enum: format.CreateEnumFormatter(
|
||||
[]string{
|
||||
"None",
|
||||
"Markdown",
|
||||
"HTML",
|
||||
"MarkdownV2",
|
||||
}),
|
||||
}
|
||||
|
||||
type parseMode int
|
||||
|
||||
type parseModeVals struct {
|
||||
None parseMode
|
||||
Markdown parseMode
|
||||
HTML parseMode
|
||||
MarkdownV2 parseMode
|
||||
Enum types.EnumFormatter
|
||||
}
|
||||
|
||||
func (pm parseMode) String() string {
|
||||
return ParseModes.Enum.Print(int(pm))
|
||||
}
|
187
pkg/services/telegram/telegram_test.go
Normal file
187
pkg/services/telegram/telegram_test.go
Normal file
|
@ -0,0 +1,187 @@
|
|||
package telegram
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jarcoal/httpmock"
|
||||
"github.com/onsi/ginkgo/v2"
|
||||
"github.com/onsi/gomega"
|
||||
|
||||
"github.com/nicholas-fedor/shoutrrr/internal/testutils"
|
||||
)
|
||||
|
||||
func TestTelegram(t *testing.T) {
|
||||
gomega.RegisterFailHandler(ginkgo.Fail)
|
||||
ginkgo.RunSpecs(t, "Shoutrrr Telegram Suite")
|
||||
}
|
||||
|
||||
var (
|
||||
envTelegramURL string
|
||||
logger *log.Logger
|
||||
|
||||
_ = ginkgo.BeforeSuite(func() {
|
||||
envTelegramURL = os.Getenv("SHOUTRRR_TELEGRAM_URL")
|
||||
logger = log.New(ginkgo.GinkgoWriter, "Test", log.LstdFlags)
|
||||
})
|
||||
)
|
||||
|
||||
var _ = ginkgo.Describe("the telegram service", func() {
|
||||
var telegram *Service // No telegram. prefix needed
|
||||
|
||||
ginkgo.BeforeEach(func() {
|
||||
telegram = &Service{}
|
||||
})
|
||||
|
||||
ginkgo.When("running integration tests", func() {
|
||||
ginkgo.It("should not error out", func() {
|
||||
if envTelegramURL == "" {
|
||||
return
|
||||
}
|
||||
serviceURL, _ := url.Parse(envTelegramURL)
|
||||
err := telegram.Initialize(serviceURL, logger)
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
err = telegram.Send("This is an integration test Message", nil)
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
})
|
||||
ginkgo.When("given a Message that exceeds the max length", func() {
|
||||
ginkgo.It("should generate an error", func() {
|
||||
if envTelegramURL == "" {
|
||||
return
|
||||
}
|
||||
hundredChars := "this string is exactly (to the letter) a hundred characters long which will make the send func error"
|
||||
serviceURL, _ := url.Parse("telegram://12345:mock-token@telegram/?chats=channel-1")
|
||||
builder := strings.Builder{}
|
||||
for range 42 {
|
||||
builder.WriteString(hundredChars)
|
||||
}
|
||||
|
||||
err := telegram.Initialize(serviceURL, logger)
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
err = telegram.Send(builder.String(), nil)
|
||||
gomega.Expect(err).To(gomega.HaveOccurred())
|
||||
})
|
||||
})
|
||||
ginkgo.When("given a valid request with a faked token", func() {
|
||||
if envTelegramURL == "" {
|
||||
return
|
||||
}
|
||||
ginkgo.It("should generate a 401", func() {
|
||||
serviceURL, _ := url.Parse(
|
||||
"telegram://000000000:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@telegram/?chats=channel-id",
|
||||
)
|
||||
message := "this is a perfectly valid Message"
|
||||
|
||||
err := telegram.Initialize(serviceURL, logger)
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
err = telegram.Send(message, nil)
|
||||
gomega.Expect(err).To(gomega.HaveOccurred())
|
||||
gomega.Expect(strings.Contains(err.Error(), "401 Unauthorized")).To(gomega.BeTrue())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Describe("creating configurations", func() {
|
||||
ginkgo.When("given an url", func() {
|
||||
ginkgo.It("should return an error if no arguments where supplied", func() {
|
||||
expectErrorAndEmptyObject(telegram, "telegram://", logger)
|
||||
})
|
||||
ginkgo.It("should return an error if the token has an invalid format", func() {
|
||||
expectErrorAndEmptyObject(telegram, "telegram://invalid-token", logger)
|
||||
})
|
||||
ginkgo.It("should return an error if only the api token where supplied", func() {
|
||||
expectErrorAndEmptyObject(telegram, "telegram://12345:mock-token@telegram", logger)
|
||||
})
|
||||
|
||||
ginkgo.When("the url is valid", func() {
|
||||
var config *Config // No telegram. prefix
|
||||
var err error
|
||||
|
||||
ginkgo.BeforeEach(func() {
|
||||
serviceURL, _ := url.Parse(
|
||||
"telegram://12345:mock-token@telegram/?chats=channel-1,channel-2,channel-3",
|
||||
)
|
||||
err = telegram.Initialize(serviceURL, logger)
|
||||
config = telegram.GetConfig()
|
||||
})
|
||||
|
||||
ginkgo.It("should create a config object", func() {
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
gomega.Expect(config).ToNot(gomega.BeNil())
|
||||
})
|
||||
ginkgo.It("should create a config object containing the API Token", func() {
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
gomega.Expect(config.Token).To(gomega.Equal("12345:mock-token"))
|
||||
})
|
||||
ginkgo.It("should add every chats query field as a chat ID", func() {
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
gomega.Expect(config.Chats).To(gomega.Equal([]string{
|
||||
"channel-1",
|
||||
"channel-2",
|
||||
"channel-3",
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Describe("sending the payload", func() {
|
||||
var err error
|
||||
ginkgo.BeforeEach(func() {
|
||||
httpmock.Activate()
|
||||
})
|
||||
ginkgo.AfterEach(func() {
|
||||
httpmock.DeactivateAndReset()
|
||||
})
|
||||
ginkgo.It("should not report an error if the server accepts the payload", func() {
|
||||
serviceURL, _ := url.Parse(
|
||||
"telegram://12345:mock-token@telegram/?chats=channel-1,channel-2,channel-3",
|
||||
)
|
||||
err = telegram.Initialize(serviceURL, logger)
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
|
||||
setupResponder("sendMessage", telegram.GetConfig().Token, 200, "")
|
||||
|
||||
err = telegram.Send("Message", nil)
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.It("should implement basic service API methods correctly", func() {
|
||||
serviceURL, _ := url.Parse("telegram://12345:mock-token@telegram/?chats=channel-1")
|
||||
err := telegram.Initialize(serviceURL, logger)
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
|
||||
config := telegram.GetConfig()
|
||||
testutils.TestConfigGetInvalidQueryValue(config)
|
||||
testutils.TestConfigSetInvalidQueryValue(
|
||||
config,
|
||||
"telegram://12345:mock-token@telegram/?chats=channel-1&foo=bar",
|
||||
)
|
||||
testutils.TestConfigGetEnumsCount(config, 1)
|
||||
testutils.TestConfigGetFieldsCount(config, 6)
|
||||
})
|
||||
ginkgo.It("should return the correct service ID", func() {
|
||||
service := &Service{}
|
||||
gomega.Expect(service.GetID()).To(gomega.Equal("telegram"))
|
||||
})
|
||||
})
|
||||
|
||||
func expectErrorAndEmptyObject(telegram *Service, rawURL string, logger *log.Logger) {
|
||||
serviceURL, _ := url.Parse(rawURL)
|
||||
err := telegram.Initialize(serviceURL, logger)
|
||||
gomega.Expect(err).To(gomega.HaveOccurred())
|
||||
|
||||
config := telegram.GetConfig()
|
||||
gomega.Expect(config.Token).To(gomega.BeEmpty())
|
||||
gomega.Expect(config.Chats).To(gomega.BeEmpty())
|
||||
}
|
||||
|
||||
func setupResponder(endpoint string, token string, code int, body string) {
|
||||
targetURL := fmt.Sprintf("https://api.telegram.org/bot%s/%s", token, endpoint)
|
||||
httpmock.RegisterResponder("POST", targetURL, httpmock.NewStringResponder(code, body))
|
||||
}
|
10
pkg/services/telegram/telegram_token.go
Normal file
10
pkg/services/telegram/telegram_token.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package telegram
|
||||
|
||||
import "regexp"
|
||||
|
||||
// IsTokenValid for use with telegram.
|
||||
func IsTokenValid(token string) bool {
|
||||
matched, err := regexp.MatchString("^[0-9]+:[a-zA-Z0-9_-]+$", token)
|
||||
|
||||
return matched && err == nil
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue