From 79c4f9942453c1a973f1b750a53d2f3a6d8ff1f7 Mon Sep 17 00:00:00 2001 From: Roman Volykh Date: Mon, 5 Jan 2026 12:39:58 +0200 Subject: [PATCH] feat(poweron): Change UX / wip --- apps/poweron/config/config.go | 30 ++++ apps/poweron/main.go | 212 +++++++++---------------- apps/poweron/operations/menu.go | 81 ++++++++++ apps/poweron/operations/subscribe.go | 14 ++ apps/poweron/operations/today.go | 23 +++ apps/poweron/operations/tomorrow.go | 14 ++ apps/poweron/operations/unsubscribe.go | 14 ++ apps/poweron/reply/send_menu.go | 19 +++ apps/poweron/reply/send_message.go | 16 ++ apps/poweron/reply/telegram.go | 22 +++ apps/poweron/scrap/html.go | 31 ++++ apps/poweron/scrap/model.go | 14 ++ apps/poweron/scrap/poweron.go | 94 +++++++++++ 13 files changed, 445 insertions(+), 139 deletions(-) create mode 100644 apps/poweron/config/config.go create mode 100644 apps/poweron/operations/menu.go create mode 100644 apps/poweron/operations/subscribe.go create mode 100644 apps/poweron/operations/today.go create mode 100644 apps/poweron/operations/tomorrow.go create mode 100644 apps/poweron/operations/unsubscribe.go create mode 100644 apps/poweron/reply/send_menu.go create mode 100644 apps/poweron/reply/send_message.go create mode 100644 apps/poweron/reply/telegram.go create mode 100644 apps/poweron/scrap/html.go create mode 100644 apps/poweron/scrap/model.go create mode 100644 apps/poweron/scrap/poweron.go diff --git a/apps/poweron/config/config.go b/apps/poweron/config/config.go new file mode 100644 index 0000000..0ec19bb --- /dev/null +++ b/apps/poweron/config/config.go @@ -0,0 +1,30 @@ +package config + +import ( + "context" + "os" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" +) + +type Config struct { + TelegramAPIToken string + SSMParamTelegramAPIToken string + SSMParamPowerScheduleCache string + PowerScheduleCacheTTL time.Duration +} + +func LoadAppConfig() Config { + return Config{ + TelegramAPIToken: os.Getenv("TELEGRAM_APITOKEN"), + SSMParamTelegramAPIToken: os.Getenv("SSM_PARAM_TELEGRAM_APITOKEN"), + SSMParamPowerScheduleCache: os.Getenv("SSM_PARAM_POWERON_CACHE"), + PowerScheduleCacheTTL: 5 * time.Minute, + } +} + +func LoadAWSConfig(ctx context.Context) (aws.Config, error) { + return config.LoadDefaultConfig(ctx) +} diff --git a/apps/poweron/main.go b/apps/poweron/main.go index 049b505..2257dd4 100644 --- a/apps/poweron/main.go +++ b/apps/poweron/main.go @@ -5,28 +5,20 @@ import ( "encoding/json" "fmt" "log" - "net/http" - "os" - "strings" - "time" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/ssm" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" -) - -const ( - PowerScheduleCacheTTL = 5 * time.Minute - TelegramAPIToken = "TELEGRAM_APITOKEN" - SSMParamTelegramAPIToken = "SSM_PARAM_TELEGRAM_APITOKEN" - SSMParamPowerScheduleCache = "SSM_PARAM_POWERON_CACHE" + "github.com/rvolykh/telegram-bot/apps/poweron/config" + "github.com/rvolykh/telegram-bot/apps/poweron/operations" + "github.com/rvolykh/telegram-bot/apps/poweron/reply" ) type LambdaFunction struct { SSMClient *ssm.Client + Config config.Config } func (f *LambdaFunction) Handler(ctx context.Context, sqsEvent events.SQSEvent) error { @@ -36,14 +28,10 @@ func (f *LambdaFunction) Handler(ctx context.Context, sqsEvent events.SQSEvent) if err != nil { return fmt.Errorf("failed to get API token: %w", err) } - bot, err := tgbotapi.NewBotAPI(apiToken) - if err != nil { - return fmt.Errorf("failed to create bot: %w", err) - } - schedule, err := f.getPowerSchedule(ctx) + telegram, err := reply.NewTelegram(apiToken) if err != nil { - return fmt.Errorf("failed to get power on: %w", err) + return fmt.Errorf("failed to create telegram: %w", err) } for i, record := range sqsEvent.Records { @@ -59,9 +47,6 @@ func (f *LambdaFunction) Handler(ctx context.Context, sqsEvent events.SQSEvent) continue } - log.Printf("Parsed Telegram Update:") - log.Printf(" Update ID: %d", update.UpdateID) - if update.Message == nil { log.Printf("Skipping update: no message") continue @@ -74,24 +59,80 @@ func (f *LambdaFunction) Handler(ctx context.Context, sqsEvent events.SQSEvent) chatID := update.Message.Chat.ID - _, err := bot.Send(tgbotapi.NewMessage(chatID, schedule)) - if err != nil { - log.Printf("Error sending message: %v", err) - continue + args := update.Message.CommandArguments() + switch args { + case "": + err = operations.ShowMainMenu(ctx, telegram, chatID) + if err != nil { + log.Printf("Error showing main menu: %v", err) + continue + } + log.Printf("Main menu sent to chat: %d", chatID) + + case "Сьогодні": + err = operations.Today(ctx, f.Config, telegram, f.SSMClient, chatID) + if err != nil { + log.Printf("Error showing today schedule: %v", err) + continue + } + log.Printf("Today schedule sent to chat: %d", chatID) + + case "Завтра": + err = operations.Tomorrow(ctx, f.Config, telegram, f.SSMClient, chatID) + if err != nil { + log.Printf("Error showing tomorrow schedule: %v", err) + continue + } + log.Printf("Tomorrow schedule sent to chat: %d", chatID) + + case "Підписатись": + err = operations.ShowSelectGroupMenu(ctx, telegram, chatID) + if err != nil { + log.Printf("Error subscribing: %v", err) + continue + } + log.Printf("Select group menu sent to chat: %d", chatID) + + case "Відписатись": + err = operations.Unsubscribe(ctx, f.Config, telegram, f.SSMClient, chatID) + if err != nil { + log.Printf("Error unsubscribing: %v", err) + continue + } + log.Printf("Unsubscribed from chat: %d", chatID) + + case "Закрити": + err = operations.CloseMenu(ctx, telegram, chatID) + if err != nil { + log.Printf("Error closing menu: %v", err) + continue + } + log.Printf("Closed menu in chat: %d", chatID) + + case "1.1", "1.2", "2.1", "2.2", "3.1", "3.2", "4.1", "4.2", "5.1", "5.2", "6.1", "6.2": + err = operations.Subscribe(ctx, f.Config, telegram, f.SSMClient, chatID, args) + if err != nil { + log.Printf("Error showing group schedule: %v", err) + continue + } + log.Printf("Subscribed to group %s in chat: %d", args, chatID) + + default: + log.Printf("Unknown command args: %s", args) + } - log.Printf("Message sent to user: %s", update.FromChat().UserName) } return nil } func (f *LambdaFunction) getAPIToken(ctx context.Context) (string, error) { - if apiToken, ok := os.LookupEnv(TelegramAPIToken); ok { - return apiToken, nil + if f.Config.TelegramAPIToken != "" { + return f.Config.TelegramAPIToken, nil } apiToken, err := f.SSMClient.GetParameter(ctx, &ssm.GetParameterInput{ - Name: aws.String(os.Getenv(SSMParamTelegramAPIToken)), + Name: aws.String(f.Config.SSMParamTelegramAPIToken), WithDecryption: aws.Bool(true), }) if err != nil { @@ -100,122 +141,15 @@ func (f *LambdaFunction) getAPIToken(ctx context.Context) (string, error) { return *apiToken.Parameter.Value, nil } -func (f *LambdaFunction) getPowerSchedule(ctx context.Context) (string, error) { - powerSchedule, err := f.SSMClient.GetParameter(ctx, &ssm.GetParameterInput{ - Name: aws.String(os.Getenv(SSMParamPowerScheduleCache)), - WithDecryption: aws.Bool(true), - }) - if err != nil { - log.Printf("Failed to get power schedule from cache: %v", err) - } else { - isCacheValid := powerSchedule.Parameter.Value != nil && - *powerSchedule.Parameter.Value != "none" && - powerSchedule.Parameter.LastModifiedDate.After(time.Now().Add(-PowerScheduleCacheTTL)) - - if isCacheValid { - log.Printf("Power schedule is still valid, returning cached value") - return *powerSchedule.Parameter.Value, nil - } - log.Printf("Power schedule is outdated, refreshing cache") - } - - powerScheduleText, err := getPowerSchedule() - if err != nil { - return "", fmt.Errorf("failed to get power schedule: %w", err) - } - - _, err = f.SSMClient.PutParameter(ctx, &ssm.PutParameterInput{ - Name: aws.String(os.Getenv(SSMParamPowerScheduleCache)), - Value: aws.String(powerScheduleText), - Type: "SecureString", - Overwrite: aws.Bool(true), - }) - if err != nil { - log.Printf("Failed to put power schedule to cache: %v", err) - } else { - log.Printf("Updated power schedule in cache") - } - - return powerScheduleText, nil -} - -type PowerOnMenuItem struct { - Name string `json:"name"` - RawMobileHTML string `json:"rawMobileHtml"` -} - -type PowerOnMember struct { - MenuItems []PowerOnMenuItem `json:"menuItems"` -} - -type PowerOnResponse struct { - Member []PowerOnMember `json:"hydra:member"` -} - -func getPowerSchedule() (string, error) { - resp, err := http.Get("https://api.loe.lviv.ua/api/menus?page=1&type=photo-grafic") - if err != nil { - return "", fmt.Errorf("failed to get power on: %w", err) - } - defer resp.Body.Close() - - var powerOnResponse PowerOnResponse - err = json.NewDecoder(resp.Body).Decode(&powerOnResponse) - if err != nil { - return "", fmt.Errorf("failed to decode power on response: %w", err) - } - - if len(powerOnResponse.Member) == 0 { - return "", fmt.Errorf("no power on found") - } - if len(powerOnResponse.Member[0].MenuItems) == 0 { - return "", fmt.Errorf("no menu items found") - } - - text := cleanHTML(powerOnResponse.Member[0].MenuItems[0].RawMobileHTML) - if text == "" { - text = "Немає запланованих відключень електроенергії на сьогодні." - } - - return text, nil -} - -func cleanHTML(text string) string { - var builder strings.Builder - builder.Grow(len(text)) - - in := false // True if we are inside an HTML tag. - start := 0 // The index of the previous start tag character `<` - end := 0 // The index of the previous end tag character `>` - for i, c := range text { - if (i+1) == len(text) && end >= start { - builder.WriteString(text[end:]) - } - if c != '<' && c != '>' { - continue - } - if c == '<' { - if !in { - start = i - builder.WriteString(text[end:start]) - } - in = true - continue - } - end, in = i+1, false - } - - return builder.String() -} - func main() { - cfg, err := config.LoadDefaultConfig(context.TODO()) + cfg, err := config.LoadAWSConfig(context.TODO()) if err != nil { - log.Printf("unable to load SDK config: %v", err) + log.Printf("unable to load AWS config: %v", err) } fn := &LambdaFunction{ SSMClient: ssm.NewFromConfig(cfg), + Config: config.LoadAppConfig(), } lambda.Start(fn.Handler) } diff --git a/apps/poweron/operations/menu.go b/apps/poweron/operations/menu.go new file mode 100644 index 0000000..d2555fd --- /dev/null +++ b/apps/poweron/operations/menu.go @@ -0,0 +1,81 @@ +package operations + +import ( + "context" + "fmt" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "github.com/rvolykh/telegram-bot/apps/poweron/reply" +) + +var ( + keyboardMainMenu = tgbotapi.NewOneTimeReplyKeyboard( + tgbotapi.NewKeyboardButtonRow( + tgbotapi.NewKeyboardButton("/poweron Сьогодні"), + tgbotapi.NewKeyboardButton("/poweron Завтра"), + ), + tgbotapi.NewKeyboardButtonRow( + tgbotapi.NewKeyboardButton("/poweron Підписатись"), + tgbotapi.NewKeyboardButton("/poweron Відписатись"), + ), + tgbotapi.NewKeyboardButtonRow( + tgbotapi.NewKeyboardButton("/poweron Закрити"), + ), + ) + + keyboardGroupsMenu = tgbotapi.NewOneTimeReplyKeyboard( + tgbotapi.NewKeyboardButtonRow( + tgbotapi.NewKeyboardButton("/poweron 1.1"), + tgbotapi.NewKeyboardButton("/poweron 1.2"), + ), + tgbotapi.NewKeyboardButtonRow( + tgbotapi.NewKeyboardButton("/poweron 2.1"), + tgbotapi.NewKeyboardButton("/poweron 2.2"), + ), + tgbotapi.NewKeyboardButtonRow( + tgbotapi.NewKeyboardButton("/poweron 3.1"), + tgbotapi.NewKeyboardButton("/poweron 3.2"), + ), + tgbotapi.NewKeyboardButtonRow( + tgbotapi.NewKeyboardButton("/poweron 4.1"), + tgbotapi.NewKeyboardButton("/poweron 4.2"), + ), + tgbotapi.NewKeyboardButtonRow( + tgbotapi.NewKeyboardButton("/poweron 5.1"), + tgbotapi.NewKeyboardButton("/poweron 5.2"), + ), + tgbotapi.NewKeyboardButtonRow( + tgbotapi.NewKeyboardButton("/poweron 6.1"), + tgbotapi.NewKeyboardButton("/poweron 6.2"), + ), + tgbotapi.NewKeyboardButtonRow( + tgbotapi.NewKeyboardButton("/poweron Закрити"), + ), + ) + + keyboardCloseMenu = tgbotapi.NewRemoveKeyboard(true) +) + +func ShowMainMenu(ctx context.Context, t *reply.Telegram, chatID int64) error { + err := t.SendMenu(ctx, chatID, "Оберіть операцію", keyboardMainMenu) + if err != nil { + return fmt.Errorf("failed to send main menu: %w", err) + } + return nil +} + +func ShowSelectGroupMenu(ctx context.Context, t *reply.Telegram, chatID int64) error { + err := t.SendMenu(ctx, chatID, "Оберіть групу", keyboardGroupsMenu) + if err != nil { + return fmt.Errorf("failed to send select group menu: %w", err) + } + return nil +} + +func CloseMenu(ctx context.Context, t *reply.Telegram, chatID int64) error { + err := t.SendMenu(ctx, chatID, "Закрито", keyboardCloseMenu) + if err != nil { + return fmt.Errorf("failed to send close menu: %w", err) + } + return nil +} diff --git a/apps/poweron/operations/subscribe.go b/apps/poweron/operations/subscribe.go new file mode 100644 index 0000000..27f1561 --- /dev/null +++ b/apps/poweron/operations/subscribe.go @@ -0,0 +1,14 @@ +package operations + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/ssm" + "github.com/rvolykh/telegram-bot/apps/poweron/config" + "github.com/rvolykh/telegram-bot/apps/poweron/reply" +) + +func Subscribe(ctx context.Context, cfg config.Config, t *reply.Telegram, ssmClient *ssm.Client, chatID int64, group string) error { + // TODO: Implement subscription logic + return t.SendMessage(ctx, chatID, "Підписалися на групу "+group) +} diff --git a/apps/poweron/operations/today.go b/apps/poweron/operations/today.go new file mode 100644 index 0000000..495dfa4 --- /dev/null +++ b/apps/poweron/operations/today.go @@ -0,0 +1,23 @@ +package operations + +import ( + "context" + "fmt" + "time" + + "github.com/aws/aws-sdk-go-v2/service/ssm" + "github.com/rvolykh/telegram-bot/apps/poweron/config" + "github.com/rvolykh/telegram-bot/apps/poweron/reply" + "github.com/rvolykh/telegram-bot/apps/poweron/scrap" +) + +func Today(ctx context.Context, cfg config.Config, t *reply.Telegram, ssmClient *ssm.Client, chatID int64) error { + poweron := scrap.NewPoweron(ssmClient, cfg.SSMParamPowerScheduleCache, 1*time.Hour) + + powerSchedule, err := poweron.GetPowerSchedule(ctx) + if err != nil { + return fmt.Errorf("failed to get power schedule: %w", err) + } + + return t.SendMessage(ctx, chatID, powerSchedule) +} diff --git a/apps/poweron/operations/tomorrow.go b/apps/poweron/operations/tomorrow.go new file mode 100644 index 0000000..ea2771c --- /dev/null +++ b/apps/poweron/operations/tomorrow.go @@ -0,0 +1,14 @@ +package operations + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/ssm" + "github.com/rvolykh/telegram-bot/apps/poweron/config" + "github.com/rvolykh/telegram-bot/apps/poweron/reply" +) + +func Tomorrow(ctx context.Context, cfg config.Config, t *reply.Telegram, ssmClient *ssm.Client, chatID int64) error { + // TODO: Implement tomorrow logic + return t.SendMessage(ctx, chatID, "Дана операція ще не реалізована") +} diff --git a/apps/poweron/operations/unsubscribe.go b/apps/poweron/operations/unsubscribe.go new file mode 100644 index 0000000..edea277 --- /dev/null +++ b/apps/poweron/operations/unsubscribe.go @@ -0,0 +1,14 @@ +package operations + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/ssm" + "github.com/rvolykh/telegram-bot/apps/poweron/config" + "github.com/rvolykh/telegram-bot/apps/poweron/reply" +) + +func Unsubscribe(ctx context.Context, cfg config.Config, t *reply.Telegram, ssmClient *ssm.Client, chatID int64) error { + // TODO: Implement unsubscribe logic + return t.SendMessage(ctx, chatID, "Відписалися від усіх груп") +} diff --git a/apps/poweron/reply/send_menu.go b/apps/poweron/reply/send_menu.go new file mode 100644 index 0000000..c029874 --- /dev/null +++ b/apps/poweron/reply/send_menu.go @@ -0,0 +1,19 @@ +package reply + +import ( + "context" + "fmt" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +func (t *Telegram) SendMenu(ctx context.Context, chatID int64, message string, keyboard interface{}) error { + msg := tgbotapi.NewMessage(chatID, message) + msg.ReplyMarkup = keyboard + + _, err := t.bot.Send(msg) + if err != nil { + return fmt.Errorf("failed to send menu: %w", err) + } + return nil +} diff --git a/apps/poweron/reply/send_message.go b/apps/poweron/reply/send_message.go new file mode 100644 index 0000000..0b12a1c --- /dev/null +++ b/apps/poweron/reply/send_message.go @@ -0,0 +1,16 @@ +package reply + +import ( + "context" + "fmt" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +func (t *Telegram) SendMessage(ctx context.Context, chatID int64, message string) error { + _, err := t.bot.Send(tgbotapi.NewMessage(chatID, message)) + if err != nil { + return fmt.Errorf("failed to send message: %w", err) + } + return nil +} diff --git a/apps/poweron/reply/telegram.go b/apps/poweron/reply/telegram.go new file mode 100644 index 0000000..c41d05e --- /dev/null +++ b/apps/poweron/reply/telegram.go @@ -0,0 +1,22 @@ +package reply + +import ( + "fmt" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +type Telegram struct { + bot *tgbotapi.BotAPI +} + +func NewTelegram(apiToken string) (*Telegram, error) { + bot, err := tgbotapi.NewBotAPI(apiToken) + if err != nil { + return nil, fmt.Errorf("failed to create bot: %w", err) + } + + return &Telegram{ + bot: bot, + }, nil +} diff --git a/apps/poweron/scrap/html.go b/apps/poweron/scrap/html.go new file mode 100644 index 0000000..5a94372 --- /dev/null +++ b/apps/poweron/scrap/html.go @@ -0,0 +1,31 @@ +package scrap + +import "strings" + +func cleanHTML(text string) string { + var builder strings.Builder + builder.Grow(len(text)) + + in := false // True if we are inside an HTML tag. + start := 0 // The index of the previous start tag character `<` + end := 0 // The index of the previous end tag character `>` + for i, c := range text { + if (i+1) == len(text) && end >= start { + builder.WriteString(text[end:]) + } + if c != '<' && c != '>' { + continue + } + if c == '<' { + if !in { + start = i + builder.WriteString(text[end:start]) + } + in = true + continue + } + end, in = i+1, false + } + + return builder.String() +} diff --git a/apps/poweron/scrap/model.go b/apps/poweron/scrap/model.go new file mode 100644 index 0000000..175dbba --- /dev/null +++ b/apps/poweron/scrap/model.go @@ -0,0 +1,14 @@ +package scrap + +type PowerOnMenuItem struct { + Name string `json:"name"` + RawMobileHTML string `json:"rawMobileHtml"` +} + +type PowerOnMember struct { + MenuItems []PowerOnMenuItem `json:"menuItems"` +} + +type PowerOnResponse struct { + Member []PowerOnMember `json:"hydra:member"` +} diff --git a/apps/poweron/scrap/poweron.go b/apps/poweron/scrap/poweron.go new file mode 100644 index 0000000..142aa1f --- /dev/null +++ b/apps/poweron/scrap/poweron.go @@ -0,0 +1,94 @@ +package scrap + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ssm" +) + +type Poweron struct { + ssmClient *ssm.Client + cacheKey string + cacheTTL time.Duration +} + +func NewPoweron(ssmClient *ssm.Client, cacheKey string, cacheTTL time.Duration) *Poweron { + return &Poweron{ + ssmClient: ssmClient, + cacheKey: cacheKey, + cacheTTL: cacheTTL, + } +} + +func (p *Poweron) GetPowerSchedule(ctx context.Context) (string, error) { + powerSchedule, err := p.ssmClient.GetParameter(ctx, &ssm.GetParameterInput{ + Name: aws.String(p.cacheKey), + WithDecryption: aws.Bool(true), + }) + if err != nil { + log.Printf("Failed to get power schedule from cache: %v", err) + } else { + isCacheValid := powerSchedule.Parameter.Value != nil && + *powerSchedule.Parameter.Value != "none" && + powerSchedule.Parameter.LastModifiedDate.After(time.Now().Add(-p.cacheTTL)) + + if isCacheValid { + log.Printf("Power schedule is still valid, returning cached value") + return *powerSchedule.Parameter.Value, nil + } + log.Printf("Power schedule is outdated, refreshing cache") + } + + powerScheduleText, err := getPowerSchedule() + if err != nil { + return "", fmt.Errorf("failed to get power schedule: %w", err) + } + + _, err = p.ssmClient.PutParameter(ctx, &ssm.PutParameterInput{ + Name: aws.String(p.cacheKey), + Value: aws.String(powerScheduleText), + Type: "SecureString", + Overwrite: aws.Bool(true), + }) + if err != nil { + log.Printf("Failed to put power schedule to cache: %v", err) + } else { + log.Printf("Updated power schedule in cache") + } + + return powerScheduleText, nil +} + +func getPowerSchedule() (string, error) { + resp, err := http.Get("https://api.loe.lviv.ua/api/menus?page=1&type=photo-grafic") + if err != nil { + return "", fmt.Errorf("failed to get power on: %w", err) + } + defer resp.Body.Close() + + var powerOnResponse PowerOnResponse + err = json.NewDecoder(resp.Body).Decode(&powerOnResponse) + if err != nil { + return "", fmt.Errorf("failed to decode power on response: %w", err) + } + + if len(powerOnResponse.Member) == 0 { + return "", fmt.Errorf("no power on found") + } + if len(powerOnResponse.Member[0].MenuItems) == 0 { + return "", fmt.Errorf("no menu items found") + } + + text := cleanHTML(powerOnResponse.Member[0].MenuItems[0].RawMobileHTML) + if text == "" { + text = "Немає запланованих відключень електроенергії" + } + + return text, nil +}