From a9246af62ddf5dbb1452c1eb54ab6501229a311f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Fri, 27 May 2022 12:52:18 +0200 Subject: [PATCH] fix(telegram): update docs and generator for private channels (#250) --- docs/services/telegram.md | 19 +++++-- pkg/services/telegram/telegram_generator.go | 50 ++++++++++++------- .../telegram/telegram_generator_test.go | 13 ++++- pkg/services/telegram/telegram_json.go | 41 ++++++++++++--- 4 files changed, 92 insertions(+), 31 deletions(-) diff --git a/docs/services/telegram.md b/docs/services/telegram.md index 73d92e11..b887e630 100644 --- a/docs/services/telegram.md +++ b/docs/services/telegram.md @@ -15,15 +15,18 @@ Talk to [the botfather](https://core.telegram.org/bots#6-botfather). The `chats` param consists of one or more `Chat ID`s or `channel name`s. -### Channels -The channel names can be retrieved in the telegram client in the `Channel info` section. +### Public Channels +The channel names can be retrieved in the telegram client in the `Channel info` section for public channels. Replace the `t.me/` prefix from the link with a `@`. !!! note Channels names need to be prefixed by `@` to identify them as such. +!!! note + If your channel only has an invite link (starting with `t.me/+`), you have to use it's Chat ID (see below) + ### Chats -Group chats and private chats are identifieds by `Chat ID`s. Unfortunatly, they are generally not visible in the +Private channels, Group chats and private chats are identified by `Chat ID`s. Unfortunatly, they are generally not visible in the telegram clients. The easiest way to retrieve them is by using the `shoutrrr generate telegram` command which will guide you through creating a URL with your target chats. @@ -34,6 +37,16 @@ creating a URL with your target chats. docker run --rm -it containrrr/shoutrrr generate telegram ``` +### Asking @shoutrrrbot +Another way of retrieving the Chat IDs, is by forwarding a message from the target chat to the [@shoutrrrbot](https://t.me/shoutrrrbot). +It will reply with the Chat ID for the chat where the forwarded message was originally posted. +Note that it will not work correctly for Group chats, as those messages are just seen as being posted by a user, not in a specific chat. +Instead you can use the second method, which is to invite the @shoutrrrbot into your group chat and address a message to it (start the message with @shoutrrrbot). You can then safely kick the bot from the group. + +The bot should be constantly online, unless it's usage exceeds the free tier on GCP. It's source is available at [github.com/containrrr/shoutrrrbot](https://github.com/containrrr/shoutrrrbot). + + + ## Optional parameters You can optionally specify the __`notification`__, __`parseMode`__ and __`preview`__ parameters in the URL: diff --git a/pkg/services/telegram/telegram_generator.go b/pkg/services/telegram/telegram_generator.go index db6f2411..83dfdef6 100644 --- a/pkg/services/telegram/telegram_generator.go +++ b/pkg/services/telegram/telegram_generator.go @@ -15,17 +15,15 @@ import ( // Generator is the telegram-specific URL generator type Generator struct { - ud *generator.UserDialog - client *Client - chats []string - chatNames []string - chatTypes []string - done bool - owner *User - statusMessage int64 - botName string - Reader io.Reader - Writer io.Writer + ud *generator.UserDialog + client *Client + chats []string + chatNames []string + chatTypes []string + done bool + botName string + Reader io.Reader + Writer io.Writer } // Generate a telegram Shoutrrr configuration from a user dialog @@ -47,7 +45,6 @@ func (g *Generator) Generate(_ types.Service, props map[string]string, _ []strin token := ud.QueryString("Enter your bot token:", generator.ValidateFormat(IsTokenValid), "token") ud.Writeln("Fetching bot info...") - // ud.Writeln("Session token: %v", g.sessionToken) g.client = &Client{token: token} botInfo, err := g.client.GetBotInfo() @@ -77,6 +74,9 @@ func (g *Generator) Generate(_ types.Service, props map[string]string, _ []strin panic(err) } + // If no updates were retrieved, prompt user to continue + promptDone := len(updates) == 0 + for _, update := range updates { lastUpdate = update.UpdateID + 1 @@ -88,25 +88,37 @@ func (g *Generator) Generate(_ types.Service, props map[string]string, _ []strin if message != nil { chat := message.Chat - source := message.Chat.Username + source := message.Chat.Name() if message.From != nil { - source = message.From.Username + source = "@" + message.From.Username } - ud.Writeln("Got Message '%v' from @%v in %v chat %v", + + ud.Writeln("Got Message '%v' from %v in %v chat %v", f.ColorizeString(message.Text), f.ColorizeProp(source), f.ColorizeEnum(chat.Type), f.ColorizeNumber(chat.ID)) ud.Writeln(g.addChat(chat)) + // Another chat was added, prompt user to continue + promptDone = true + } else if update.ChatMemberUpdate != nil { + cmu := update.ChatMemberUpdate + oldStatus := cmu.OldChatMember.Status + newStatus := cmu.NewChatMember.Status + ud.Writeln("Got a bot chat member update for %v, status was changed from %v to %v", + f.ColorizeProp(cmu.Chat.Name()), + f.ColorizeEnum(oldStatus), + f.ColorizeEnum(newStatus)) } else { ud.Writeln("Got unknown Update. Ignored!") } } + if promptDone { + ud.Writeln("") - ud.Writeln("") - - g.done = !ud.QueryBool(fmt.Sprintf("Got %v chat ID(s) so far. Want to add some more?", - f.ColorizeNumber(len(g.chats))), "") + g.done = !ud.QueryBool(fmt.Sprintf("Got %v chat ID(s) so far. Want to add some more?", + f.ColorizeNumber(len(g.chats))), "") + } } ud.Writeln("") diff --git a/pkg/services/telegram/telegram_generator_test.go b/pkg/services/telegram/telegram_generator_test.go index e88d8e16..3301f6fb 100644 --- a/pkg/services/telegram/telegram_generator_test.go +++ b/pkg/services/telegram/telegram_generator_test.go @@ -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" ) @@ -78,6 +79,13 @@ var _ = Describe("TelegramGenerator", func() { }{ 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!", @@ -102,6 +110,7 @@ var _ = Describe("TelegramGenerator", func() { mockTyped(mockToken) mockTyped(`no`) + Eventually(userIn).Should(gbytes.Say(`Got a bot chat member update for mockChannel, status was changed from kicked to administrator`)) Eventually(userIn).Should(gbytes.Say(`Got 1 chat ID\(s\) so far\. Want to add some more\?`)) Eventually(userIn).Should(gbytes.Say(`Selected chats:`)) Eventually(userIn).Should(gbytes.Say(`667 \(private\) @mockUser`)) diff --git a/pkg/services/telegram/telegram_json.go b/pkg/services/telegram/telegram_json.go index 87714a98..99e1e065 100644 --- a/pkg/services/telegram/telegram_json.go +++ b/pkg/services/telegram/telegram_json.go @@ -138,16 +138,19 @@ type Update struct { 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 ShippingQuery `json:"shipping_query"` //// Optional. New incoming pre-checkout query. Contains full information about checkout //PreCheckoutQuery 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 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"` - */ + //// Optional. New poll state. Bots receive only updates about stopped polls and polls, which are sent by the bot + //Poll 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 @@ -160,7 +163,7 @@ type Chat struct { // Name returns the name of the channel based on its type func (c *Chat) Name() string { - if c.Type == "private" || c.Type == "channel" { + if c.Type == "private" || c.Type == "channel" && c.Username != "" { return "@" + c.Username } return c.Title @@ -191,3 +194,27 @@ type callbackQuery struct { 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"` +}