package telegram import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "os" "strconv" "strings" "time" notconfig "github.com/tech/sendico/notification/interface/services/notification/config" "github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/model" ) const defaultAPIURL = "https://api.telegram.org" type Client interface { SendDemoRequest(ctx context.Context, request *model.DemoRequest) error } type client struct { logger mlogger.Logger httpClient *http.Client apiURL string botToken string chatID string threadID *int64 parseMode string } type sendMessagePayload struct { ChatID string `json:"chat_id"` Text string `json:"text"` ParseMode string `json:"parse_mode,omitempty"` ThreadID *int64 `json:"message_thread_id,omitempty"` DisablePreview bool `json:"disable_web_page_preview,omitempty"` DisableNotify bool `json:"disable_notification,omitempty"` ProtectContent bool `json:"protect_content,omitempty"` } func NewClient(logger mlogger.Logger, cfg *notconfig.TelegramConfig) (Client, error) { if cfg == nil { return nil, fmt.Errorf("telegram configuration is not provided") } token := strings.TrimSpace(os.Getenv(cfg.BotTokenEnv)) if token == "" { return nil, fmt.Errorf("telegram bot token env %s is empty", cfg.BotTokenEnv) } chatID := strings.TrimSpace(os.Getenv(cfg.ChatIDEnv)) if chatID == "" { return nil, fmt.Errorf("telegram chat id env %s is empty", cfg.ChatIDEnv) } var threadID *int64 if env := strings.TrimSpace(cfg.ThreadIDEnv); env != "" { raw := strings.TrimSpace(os.Getenv(env)) if raw != "" { val, err := strconv.ParseInt(raw, 10, 64) if err != nil { return nil, fmt.Errorf("telegram thread id env %s is invalid: %w", env, err) } threadID = &val } } timeout := time.Duration(cfg.TimeoutSeconds) * time.Second if timeout <= 0 { timeout = 10 * time.Second } apiURL := strings.TrimSpace(cfg.APIURL) if apiURL == "" { apiURL = defaultAPIURL } return &client{ logger: logger.Named("telegram"), httpClient: &http.Client{ Timeout: timeout, }, apiURL: strings.TrimRight(apiURL, "/"), botToken: token, chatID: chatID, threadID: threadID, parseMode: strings.TrimSpace(cfg.ParseMode), }, nil } func (c *client) SendDemoRequest(ctx context.Context, request *model.DemoRequest) error { if request == nil { return fmt.Errorf("demo request payload is nil") } message := buildMessage(request) payload := sendMessagePayload{ ChatID: c.chatID, Text: message, ParseMode: c.parseMode, ThreadID: c.threadID, DisablePreview: true, } return c.sendMessage(ctx, payload) } func (c *client) sendMessage(ctx context.Context, payload sendMessagePayload) error { body, err := json.Marshal(&payload) if err != nil { return err } req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint(), bytes.NewReader(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") resp, err := c.httpClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices { return nil } respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<10)) return fmt.Errorf("telegram sendMessage failed with status %d: %s", resp.StatusCode, string(respBody)) } func (c *client) endpoint() string { return fmt.Sprintf("%s/bot%s/sendMessage", c.apiURL, c.botToken) } func buildMessage(req *model.DemoRequest) string { var builder strings.Builder builder.WriteString("New demo request received\n") builder.WriteString(fmt.Sprintf("Name: %s\n", req.Name)) builder.WriteString(fmt.Sprintf("Organization: %s\n", req.OrganizationName)) builder.WriteString(fmt.Sprintf("Phone: %s\n", req.Phone)) builder.WriteString(fmt.Sprintf("Work email: %s\n", req.WorkEmail)) builder.WriteString(fmt.Sprintf("Payout volume: %s\n", req.PayoutVolume)) if req.Comment != "" { builder.WriteString(fmt.Sprintf("Comment: %s\n", req.Comment)) } return builder.String() }