Skip to content

Commit

Permalink
feat(generator): telegram generator/bot (#168)
Browse files Browse the repository at this point in the history
* feat(telegram): add MVP generator using bot API
* simplify bot exchange, rename channels chats
* add tests to generator and jsonclient
* remove unused telegram client parts
  • Loading branch information
piksel authored Jul 1, 2021
1 parent 17f842b commit 1e34cb3
Show file tree
Hide file tree
Showing 16 changed files with 1,015 additions and 51 deletions.
4 changes: 2 additions & 2 deletions pkg/format/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import (
// ParseBool returns true for "1","true","yes" or false for "0","false","no" or defaultValue for any other value
func ParseBool(value string, defaultValue bool) (parsedValue bool, ok bool) {
switch strings.ToLower(value) {
case "true", "1", "yes":
case "true", "1", "yes", "y":
return true, true
case "false", "0", "no":
case "false", "0", "no", "n":
return false, true
default:
return defaultValue, false
Expand Down
3 changes: 3 additions & 0 deletions pkg/format/format_colorize.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ var ColorizeError = ColorizeFalse
// ColorizeContainer colorizes the input string as "Container"
var ColorizeContainer = ColorizeDesc

// ColorizeLink colorizes the input string as "Link"
var ColorizeLink = color.New(color.FgHiBlue).SprintFunc()

// ColorizeValue colorizes the input string according to what type appears to be
func ColorizeValue(value string, isEnum bool) string {
if isEnum {
Expand Down
2 changes: 2 additions & 0 deletions pkg/generators/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import (
"fmt"
"github.com/containrrr/shoutrrr/pkg/generators/basic"
"github.com/containrrr/shoutrrr/pkg/generators/xouath2"
"github.com/containrrr/shoutrrr/pkg/services/telegram"
t "github.com/containrrr/shoutrrr/pkg/types"
"strings"
)

var generatorMap = map[string]func() t.Generator{
"basic": func() t.Generator { return &basic.Generator{} },
"oauth2": func() t.Generator { return &xouath2.Generator{} },
"telegram": func() t.Generator { return &telegram.Generator{} },
}

// NewGenerator creates an instance of the generator that corresponds to the provided identifier
Expand Down
30 changes: 8 additions & 22 deletions pkg/services/telegram/telegram.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
package telegram

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/containrrr/shoutrrr/pkg/format"
"net/http"
"net/url"

"github.com/containrrr/shoutrrr/pkg/services/standard"
"github.com/containrrr/shoutrrr/pkg/types"
)

const (
apiBase = "https://api.telegram.org/bot"
apiFormat = "https://api.telegram.org/bot%s/%s"
maxlength = 4096
)

Expand All @@ -28,7 +24,7 @@ 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")
return errors.New("Message exceeds the max length")
}

config := *service.config
Expand All @@ -55,8 +51,8 @@ func (service *Service) Initialize(configURL *url.URL, logger types.StdLogger) e
}

func (service *Service) sendMessageForChatIDs(message string, config *Config) error {
for _, channel := range service.config.Channels {
if err := sendMessageToAPI(message, channel, config); err != nil {
for _, chat := range service.config.Chats {
if err := sendMessageToAPI(message, chat, config); err != nil {
return err
}
}
Expand All @@ -68,19 +64,9 @@ func (service *Service) GetConfig() *Config {
return service.config
}

func sendMessageToAPI(message string, channel string, config *Config) error {
postURL := fmt.Sprintf("%s%s/sendMessage", apiBase, config.Token)

payload := createSendMessagePayload(message, channel, config)

jsonData, err := json.Marshal(payload)
if err != nil {
return err
}

res, err := http.Post(postURL, "application/jsonData", bytes.NewBuffer(jsonData))
if err == nil && res.StatusCode != http.StatusOK {
return fmt.Errorf("failed to send notification to \"%s\", response status code %s", channel, res.Status)
}
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
}
69 changes: 69 additions & 0 deletions pkg/services/telegram/telegram_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package telegram

import (
"encoding/json"
"fmt"
"github.com/containrrr/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 := &errorResponse{}
if err := json.Unmarshal([]byte(body), response); err == nil {
return response
}
return nil
}
10 changes: 5 additions & 5 deletions pkg/services/telegram/telegram_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ import (
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"`
Channels []string `key:"channels"`
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"`
Title string `key:"title" default:"" desc:"Notification title, optionally set by the sender"`
}

// Enums returns the fields that should use a corresponding EnumFormatter to Print/Parse their values
func (config *Config) Enums() map[string]types.EnumFormatter {
return map[string]types.EnumFormatter{
"ParseMode": parseModes.Enum,
"ParseMode": ParseModes.Enum,
}
}

Expand Down Expand Up @@ -67,7 +67,7 @@ func (config *Config) setURL(resolver types.ConfigQueryResolver, url *url.URL) e
}
}

if len(config.Channels) < 1 {
if len(config.Chats) < 1 {
return errors.New("no channels defined in config URL")
}

Expand Down
149 changes: 149 additions & 0 deletions pkg/services/telegram/telegram_generator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package telegram

import (
f "github.com/containrrr/shoutrrr/pkg/format"
"github.com/containrrr/shoutrrr/pkg/types"
"github.com/containrrr/shoutrrr/pkg/util/generator"
"os/signal"
"syscall"

"fmt"
"os"
"strconv"
)

// 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
}

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

g.ud = generator.NewUserDialog(os.Stdin, os.Stdout, props)
ud := g.ud

ud.Writeln("To start we need your bot token. If you haven't created a bot yet, you can use this link:")
ud.Writeln(" %v", f.ColorizeLink("https://t.me/botfather?start"))
ud.Writeln("")

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()
if err != nil {
return &Config{}, err
}

g.botName = botInfo.Username
ud.Writeln("")
ud.Writeln("Okay! %v will listen for any messages in PMs and group chats it is invited to.",
f.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 {

ud.Writeln("Waiting for messages to arrive...")

updates, err := g.client.GetUpdates(lastUpdate, 10, 120, nil)
if err != nil {
panic(err)
}

for _, update := range updates {
lastUpdate = update.UpdateID + 1

message := update.Message
if update.ChannelPost != nil {
message = update.ChannelPost
}

if message != nil {
chat := message.Chat

source := message.Chat.Username
if message.From != nil {
source = message.From.Username
}
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))
} else {
ud.Writeln("Got unknown Update. Ignored!")
}
}

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))), "")
}

ud.Writeln("")
ud.Writeln("Cleaning up the bot session...")

// Notify API that we got the updates
if _, err = g.client.GetUpdates(lastUpdate, 0, 0, nil); err != nil {
g.ud.Writeln("Failed to mark last updates as received: %v", f.ColorizeError(err))
}

if len(g.chats) < 1 {
return nil, fmt.Errorf("no chats were selected")
}

ud.Writeln("Selected chats:")

for i, id := range g.chats {
name := g.chatNames[i]
chatType := g.chatTypes[i]
ud.Writeln(" %v (%v) %v", f.ColorizeNumber(id), f.ColorizeEnum(chatType), f.ColorizeString(name))
}

ud.Writeln("")

config = Config{
Notification: true,
Token: token,
Chats: g.chats,
}

return &config, nil
}

func (g *Generator) addChat(chat *chat) (result string) {
id := strconv.FormatInt(chat.ID, 10)
name := chat.Name()

for _, c := range g.chats {
if c == id {
return fmt.Sprintf("chat %v is already selected!", f.ColorizeString(name))
}
}
g.chats = append(g.chats, id)
g.chatNames = append(g.chatNames, name)
g.chatTypes = append(g.chatTypes, chat.Type)

return fmt.Sprintf("Added new chat %v!", f.ColorizeString(name))
}
4 changes: 2 additions & 2 deletions pkg/services/telegram/telegram_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@ func getPayloadFromURL(testURL string, message string, logger *log.Logger) (Send
return SendMessagePayload{}, err
}

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

return createSendMessagePayload(message, telegram.config.Channels[0], telegram.config), nil
return createSendMessagePayload(message, telegram.config.Chats[0], telegram.config), nil

}

Expand Down
Loading

0 comments on commit 1e34cb3

Please sign in to comment.