package main import ( "bytes" "encoding/json" "flag" "fmt" "io" "net/http" "os" "strings" "time" ) // CloudFlare API structures type CloudFlareResponse struct { Success bool `json:"success"` Errors []CloudFlareError `json:"errors"` Result json.RawMessage `json:"result"` Messages []CloudFlareMessage `json:"messages"` } type CloudFlareError struct { Code int `json:"code"` Message string `json:"message"` } type CloudFlareMessage struct { Code int `json:"code"` Message string `json:"message"` } type DNSRecord struct { ID string `json:"id"` Type string `json:"type"` Name string `json:"name"` Content string `json:"content"` TTL int `json:"ttl"` ZoneID string `json:"zone_id"` } type Zone struct { ID string `json:"id"` Name string `json:"name"` } type TokenVerification struct { ID string `json:"id"` Status string `json:"status"` } type NotificationInfo struct { RecordName string OldIP string NewIP string IsNew bool } // Configuration type Config struct { APIToken string RecordNames []string IPSources []string DryRun bool Verbose bool Force bool TTL int TelegramBotToken string TelegramChatID string Client *http.Client } // Default IP sources var defaultIPSources = []string{ "https://ifconfig.co/ip", "https://ip.seeip.org", "https://ipv4.icanhazip.com", "https://api.ipify.org", } func main() { config := &Config{ Client: &http.Client{Timeout: 10 * time.Second}, } // Command line flags var ipSourcesFlag string var recordsFlag string var listZones bool var testTelegram bool flag.StringVar(&recordsFlag, "record", "", "DNS A record name(s) to update - comma-separated for multiple (required)") flag.StringVar(&ipSourcesFlag, "ip-sources", "", "Comma-separated list of IP detection services (optional)") flag.BoolVar(&config.DryRun, "dry-run", false, "Show what would be done without making changes") flag.BoolVar(&config.Verbose, "verbose", false, "Enable verbose logging") flag.BoolVar(&listZones, "list-zones", false, "List all accessible zones and exit") flag.BoolVar(&config.Force, "force", false, "Force update even if IP hasn't changed") flag.BoolVar(&testTelegram, "test-telegram", false, "Send a test Telegram notification and exit") flag.IntVar(&config.TTL, "ttl", 300, "TTL for DNS record in seconds") // Custom usage function flag.Usage = func() { fmt.Fprintf(os.Stderr, "CloudFlare Dynamic DNS Tool\n\n") fmt.Fprintf(os.Stderr, "Updates CloudFlare DNS A records with your current public IP address.\n") fmt.Fprintf(os.Stderr, "Supports multiple records, dry-run mode, and Telegram notifications.\n\n") fmt.Fprintf(os.Stderr, "USAGE:\n") fmt.Fprintf(os.Stderr, " %s [OPTIONS]\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, "REQUIRED ENVIRONMENT VARIABLES:\n") fmt.Fprintf(os.Stderr, " CLOUDFLARE_API_TOKEN CloudFlare API token with Zone:DNS:Edit permissions\n") fmt.Fprintf(os.Stderr, " Get from: https://dash.cloudflare.com/profile/api-tokens\n\n") fmt.Fprintf(os.Stderr, "OPTIONAL ENVIRONMENT VARIABLES:\n") fmt.Fprintf(os.Stderr, " TELEGRAM_BOT_TOKEN Telegram bot token for notifications\n") fmt.Fprintf(os.Stderr, " TELEGRAM_CHAT_ID Telegram chat ID to send notifications to\n\n") fmt.Fprintf(os.Stderr, "OPTIONS:\n") flag.PrintDefaults() fmt.Fprintf(os.Stderr, "\nEXAMPLES:\n") fmt.Fprintf(os.Stderr, " # Update single record\n") fmt.Fprintf(os.Stderr, " %s -record home.example.com\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, " # Update multiple records\n") fmt.Fprintf(os.Stderr, " %s -record \"home.example.com,api.example.com,vpn.mydomain.net\"\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, " # Dry run with verbose output\n") fmt.Fprintf(os.Stderr, " %s -dry-run -verbose -record home.example.com\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, " # Force update even if IP hasn't changed\n") fmt.Fprintf(os.Stderr, " %s -force -record home.example.com\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, " # Custom TTL and IP sources\n") fmt.Fprintf(os.Stderr, " %s -record home.example.com -ttl 600 -ip-sources \"https://ifconfig.co/ip,https://api.ipify.org\"\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, " # List accessible CloudFlare zones\n") fmt.Fprintf(os.Stderr, " %s -list-zones\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, " # Test Telegram notifications\n") fmt.Fprintf(os.Stderr, " %s -test-telegram\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, "SETUP:\n") fmt.Fprintf(os.Stderr, " 1. Create CloudFlare API token:\n") fmt.Fprintf(os.Stderr, " - Go to https://dash.cloudflare.com/profile/api-tokens\n") fmt.Fprintf(os.Stderr, " - Use 'Edit zone DNS' template\n") fmt.Fprintf(os.Stderr, " - Select your zones\n") fmt.Fprintf(os.Stderr, " - Copy token and set CLOUDFLARE_API_TOKEN environment variable\n\n") fmt.Fprintf(os.Stderr, " 2. Optional: Setup Telegram notifications:\n") fmt.Fprintf(os.Stderr, " - Message @BotFather on Telegram to create a bot\n") fmt.Fprintf(os.Stderr, " - Get your chat ID by messaging your bot, then visit:\n") fmt.Fprintf(os.Stderr, " https://api.telegram.org/bot/getUpdates\n") fmt.Fprintf(os.Stderr, " - Set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID environment variables\n\n") fmt.Fprintf(os.Stderr, "NOTES:\n") fmt.Fprintf(os.Stderr, " - Records can be in different CloudFlare zones\n") fmt.Fprintf(os.Stderr, " - Only updates when IP actually changes (unless -force is used)\n") fmt.Fprintf(os.Stderr, " - Supports both root domains and subdomains\n") fmt.Fprintf(os.Stderr, " - Telegram notifications sent only when IP changes\n") fmt.Fprintf(os.Stderr, " - Use -dry-run to test without making changes\n\n") } flag.Parse() // Validate required arguments (unless listing zones or testing telegram) if recordsFlag == "" && !listZones && !testTelegram { fmt.Fprintf(os.Stderr, "Error: -record flag is required\n") flag.Usage() os.Exit(1) } // Parse record names if recordsFlag != "" { config.RecordNames = strings.Split(recordsFlag, ",") // Trim whitespace from each record name for i, record := range config.RecordNames { config.RecordNames[i] = strings.TrimSpace(record) } } // Get API token from environment config.APIToken = os.Getenv("CLOUDFLARE_API_TOKEN") if config.APIToken == "" { fmt.Fprintf(os.Stderr, "Error: CLOUDFLARE_API_TOKEN environment variable is required\n") fmt.Fprintf(os.Stderr, "Get your API token from: https://dash.cloudflare.com/profile/api-tokens\n") fmt.Fprintf(os.Stderr, "Create a token with 'Zone:DNS:Edit' permissions for your zone\n") os.Exit(1) } // Get optional Telegram credentials config.TelegramBotToken = os.Getenv("TELEGRAM_BOT_TOKEN") config.TelegramChatID = os.Getenv("TELEGRAM_CHAT_ID") if config.Verbose && config.TelegramBotToken != "" && config.TelegramChatID != "" { fmt.Println("Telegram notifications enabled") } // Parse IP sources if ipSourcesFlag != "" { config.IPSources = strings.Split(ipSourcesFlag, ",") } else { config.IPSources = defaultIPSources } if config.Verbose { fmt.Printf("Config: Records=%v, TTL=%d, DryRun=%v, Force=%v, IPSources=%v\n", config.RecordNames, config.TTL, config.DryRun, config.Force, config.IPSources) } // If testing telegram, do that and exit (skip API token validation) if testTelegram { if err := testTelegramNotification(config); err != nil { fmt.Fprintf(os.Stderr, "Error testing Telegram: %v\n", err) os.Exit(1) } return } // Validate API token if err := validateToken(config); err != nil { fmt.Fprintf(os.Stderr, "Error validating API token: %v\n", err) os.Exit(1) } if config.Verbose { fmt.Println("API token validated successfully") } // If listing zones, do that and exit if listZones { if err := listAllZones(config); err != nil { fmt.Fprintf(os.Stderr, "Error listing zones: %v\n", err) os.Exit(1) } return } // Get current public IP currentIP, err := getCurrentIP(config) if err != nil { fmt.Fprintf(os.Stderr, "Error getting current IP: %v\n", err) os.Exit(1) } if config.Verbose { fmt.Printf("Current public IP: %s\n", currentIP) fmt.Printf("Processing %d record(s)\n", len(config.RecordNames)) } // Process each record var totalUpdates int var allNotifications []NotificationInfo for _, recordName := range config.RecordNames { if config.Verbose { fmt.Printf("\n--- Processing record: %s ---\n", recordName) } // Find the zone for the record zoneName, zoneID, err := findZoneForRecord(config, recordName) if err != nil { fmt.Fprintf(os.Stderr, "Error finding zone for %s: %v\n", recordName, err) continue } if config.Verbose { fmt.Printf("Found zone: %s (ID: %s)\n", zoneName, zoneID) } // Find existing DNS record record, err := findDNSRecordByName(config, zoneID, recordName) if err != nil { fmt.Fprintf(os.Stderr, "Error finding DNS record %s: %v\n", recordName, err) continue } // Compare IPs if record != nil { if record.Content == currentIP && !config.Force { fmt.Printf("DNS record %s already points to %s - no update needed\n", recordName, currentIP) continue } if config.Verbose { if record.Content == currentIP { fmt.Printf("DNS record %s already points to %s, but forcing update\n", recordName, currentIP) } else { fmt.Printf("DNS record %s currently points to %s, needs update to %s\n", recordName, record.Content, currentIP) } } } else { if config.Verbose { fmt.Printf("DNS record %s does not exist, will create it\n", recordName) } } // Update or create record if config.DryRun { if record != nil { if record.Content == currentIP && config.Force { fmt.Printf("DRY RUN: Would force update DNS record %s (already %s)\n", recordName, currentIP) } else { fmt.Printf("DRY RUN: Would update DNS record %s from %s to %s\n", recordName, record.Content, currentIP) } } else { fmt.Printf("DRY RUN: Would create DNS record %s with IP %s\n", recordName, currentIP) } // Collect notification info for dry-run if record == nil || record.Content != currentIP || config.Force { var oldIPForNotification string if record != nil { oldIPForNotification = record.Content } allNotifications = append(allNotifications, NotificationInfo{ RecordName: recordName, OldIP: oldIPForNotification, NewIP: currentIP, IsNew: record == nil, }) } continue } var wasUpdated bool var oldIP string if record != nil { oldIP = record.Content err = updateDNSRecordByName(config, zoneID, record.ID, recordName, currentIP) if err != nil { fmt.Fprintf(os.Stderr, "Error updating DNS record %s: %v\n", recordName, err) continue } fmt.Printf("Successfully updated DNS record %s to %s\n", recordName, currentIP) wasUpdated = true } else { err = createDNSRecordByName(config, zoneID, recordName, currentIP) if err != nil { fmt.Fprintf(os.Stderr, "Error creating DNS record %s: %v\n", recordName, err) continue } fmt.Printf("Successfully created DNS record %s with IP %s\n", recordName, currentIP) wasUpdated = true } // Collect notification info for actual updates if wasUpdated && (record == nil || oldIP != currentIP || config.Force) { allNotifications = append(allNotifications, NotificationInfo{ RecordName: recordName, OldIP: oldIP, NewIP: currentIP, IsNew: record == nil, }) totalUpdates++ } } // Send batch notification if there were any changes if len(allNotifications) > 0 { sendBatchTelegramNotification(config, allNotifications, config.DryRun) } if !config.DryRun && config.Verbose { fmt.Printf("\nProcessed %d record(s), %d update(s) made\n", len(config.RecordNames), totalUpdates) } } func validateToken(config *Config) error { req, err := http.NewRequest("GET", "https://api.cloudflare.com/client/v4/user/tokens/verify", nil) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+config.APIToken) req.Header.Set("Content-Type", "application/json") resp, err := config.Client.Do(req) if err != nil { return err } defer resp.Body.Close() var cfResp CloudFlareResponse if err := json.NewDecoder(resp.Body).Decode(&cfResp); err != nil { return err } if !cfResp.Success { return fmt.Errorf("token validation failed: %v", cfResp.Errors) } var tokenInfo TokenVerification if err := json.Unmarshal(cfResp.Result, &tokenInfo); err != nil { return err } if tokenInfo.Status != "active" { return fmt.Errorf("token is not active, status: %s", tokenInfo.Status) } return nil } func getCurrentIP(config *Config) (string, error) { var lastError error for _, source := range config.IPSources { if config.Verbose { fmt.Printf("Trying IP source: %s\n", source) } resp, err := config.Client.Get(source) if err != nil { lastError = err if config.Verbose { fmt.Printf("Failed to get IP from %s: %v\n", source, err) } continue } body, err := io.ReadAll(resp.Body) resp.Body.Close() if err != nil { lastError = err continue } if resp.StatusCode != 200 { lastError = fmt.Errorf("HTTP %d from %s", resp.StatusCode, source) continue } ip := strings.TrimSpace(string(body)) if ip != "" { return ip, nil } lastError = fmt.Errorf("empty response from %s", source) } return "", fmt.Errorf("failed to get IP from any source, last error: %v", lastError) } func findZoneForRecord(config *Config, recordName string) (string, string, error) { // Extract domain from record name (e.g., "sub.example.com" -> try "example.com", "com") parts := strings.Split(recordName, ".") if config.Verbose { fmt.Printf("Finding zone for record: %s\n", recordName) } for i := 0; i < len(parts); i++ { zoneName := strings.Join(parts[i:], ".") req, err := http.NewRequest("GET", fmt.Sprintf("https://api.cloudflare.com/client/v4/zones?name=%s", zoneName), nil) if err != nil { continue } req.Header.Set("Authorization", "Bearer "+config.APIToken) req.Header.Set("Content-Type", "application/json") resp, err := config.Client.Do(req) if err != nil { continue } var cfResp CloudFlareResponse err = json.NewDecoder(resp.Body).Decode(&cfResp) resp.Body.Close() if err != nil || !cfResp.Success { continue } var zones []Zone if err := json.Unmarshal(cfResp.Result, &zones); err != nil { continue } if len(zones) > 0 { return zones[0].Name, zones[0].ID, nil } } return "", "", fmt.Errorf("no zone found for record %s", recordName) } func findDNSRecordByName(config *Config, zoneID string, recordName string) (*DNSRecord, error) { url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records?type=A&name=%s", zoneID, recordName) req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+config.APIToken) req.Header.Set("Content-Type", "application/json") resp, err := config.Client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() var cfResp CloudFlareResponse if err := json.NewDecoder(resp.Body).Decode(&cfResp); err != nil { return nil, err } if !cfResp.Success { return nil, fmt.Errorf("API error: %v", cfResp.Errors) } var records []DNSRecord if err := json.Unmarshal(cfResp.Result, &records); err != nil { return nil, err } if len(records) == 0 { return nil, nil // Record doesn't exist } return &records[0], nil } func updateDNSRecordByName(config *Config, zoneID, recordID, recordName, ip string) error { data := map[string]interface{}{ "type": "A", "name": recordName, "content": ip, "ttl": config.TTL, } jsonData, err := json.Marshal(data) if err != nil { return err } url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records/%s", zoneID, recordID) req, err := http.NewRequest("PUT", url, bytes.NewBuffer(jsonData)) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+config.APIToken) req.Header.Set("Content-Type", "application/json") resp, err := config.Client.Do(req) if err != nil { return err } defer resp.Body.Close() var cfResp CloudFlareResponse if err := json.NewDecoder(resp.Body).Decode(&cfResp); err != nil { return err } if !cfResp.Success { return fmt.Errorf("API error: %v", cfResp.Errors) } return nil } func createDNSRecordByName(config *Config, zoneID, recordName, ip string) error { data := map[string]interface{}{ "type": "A", "name": recordName, "content": ip, "ttl": config.TTL, } jsonData, err := json.Marshal(data) if err != nil { return err } url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records", zoneID) req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+config.APIToken) req.Header.Set("Content-Type", "application/json") resp, err := config.Client.Do(req) if err != nil { return err } defer resp.Body.Close() var cfResp CloudFlareResponse if err := json.NewDecoder(resp.Body).Decode(&cfResp); err != nil { return err } if !cfResp.Success { return fmt.Errorf("API error: %v", cfResp.Errors) } return nil } func listAllZones(config *Config) error { req, err := http.NewRequest("GET", "https://api.cloudflare.com/client/v4/zones", nil) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+config.APIToken) req.Header.Set("Content-Type", "application/json") resp, err := config.Client.Do(req) if err != nil { return err } defer resp.Body.Close() var cfResp CloudFlareResponse if err := json.NewDecoder(resp.Body).Decode(&cfResp); err != nil { return err } if !cfResp.Success { return fmt.Errorf("API error: %v", cfResp.Errors) } var zones []Zone if err := json.Unmarshal(cfResp.Result, &zones); err != nil { return err } fmt.Printf("Found %d accessible zones:\n", len(zones)) for _, zone := range zones { fmt.Printf(" - %s (ID: %s)\n", zone.Name, zone.ID) } if len(zones) == 0 { fmt.Println("No zones found. Make sure your API token has Zone:Read permissions.") } return nil } func sendTelegramNotification(config *Config, record *DNSRecord, oldIP, newIP string, isDryRun bool) { // Skip if Telegram is not configured if config.TelegramBotToken == "" || config.TelegramChatID == "" { return } var message string dryRunPrefix := "" if isDryRun { dryRunPrefix = "๐Ÿงช DRY RUN - " } if record == nil { message = fmt.Sprintf("%s๐Ÿ†• DNS Record Created\n\n"+ "Record: %s\n"+ "New IP: %s\n"+ "TTL: %d seconds", dryRunPrefix, "test-record", newIP, config.TTL) } else { message = fmt.Sprintf("%s๐Ÿ”„ IP Address Changed\n\n"+ "Record: %s\n"+ "Old IP: %s\n"+ "New IP: %s\n"+ "TTL: %d seconds", dryRunPrefix, "test-record", oldIP, newIP, config.TTL) } // Prepare Telegram API request telegramURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", config.TelegramBotToken) payload := map[string]interface{}{ "chat_id": config.TelegramChatID, "text": message, "parse_mode": "HTML", } jsonData, err := json.Marshal(payload) if err != nil { if config.Verbose { fmt.Printf("Failed to marshal Telegram payload: %v\n", err) } return } // Send notification req, err := http.NewRequest("POST", telegramURL, bytes.NewBuffer(jsonData)) if err != nil { if config.Verbose { fmt.Printf("Failed to create Telegram request: %v\n", err) } return } req.Header.Set("Content-Type", "application/json") resp, err := config.Client.Do(req) if err != nil { if config.Verbose { fmt.Printf("Failed to send Telegram notification: %v\n", err) } return } defer resp.Body.Close() if resp.StatusCode == 200 { if config.Verbose { fmt.Println("Telegram notification sent successfully") } } else { if config.Verbose { body, _ := io.ReadAll(resp.Body) fmt.Printf("Telegram notification failed (HTTP %d): %s\n", resp.StatusCode, string(body)) } } } func testTelegramNotification(config *Config) error { if config.TelegramBotToken == "" || config.TelegramChatID == "" { return fmt.Errorf("Telegram not configured. Set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID environment variables") } fmt.Println("Testing Telegram notification...") // Send a test message message := "๐Ÿงช Dynamic DNS Test\n\n" + "This is a test notification from your CloudFlare Dynamic DNS tool.\n\n" + "โœ… Telegram integration is working correctly!" telegramURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", config.TelegramBotToken) payload := map[string]interface{}{ "chat_id": config.TelegramChatID, "text": message, "parse_mode": "HTML", } jsonData, err := json.Marshal(payload) if err != nil { return fmt.Errorf("failed to marshal payload: %v", err) } req, err := http.NewRequest("POST", telegramURL, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %v", err) } req.Header.Set("Content-Type", "application/json") resp, err := config.Client.Do(req) if err != nil { return fmt.Errorf("failed to send request: %v", err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if resp.StatusCode == 200 { fmt.Println("โœ… Test notification sent successfully!") if config.Verbose { fmt.Printf("Response: %s\n", string(body)) } return nil } else { return fmt.Errorf("failed to send notification (HTTP %d): %s", resp.StatusCode, string(body)) } } func sendBatchTelegramNotification(config *Config, notifications []NotificationInfo, isDryRun bool) { // Skip if Telegram is not configured if config.TelegramBotToken == "" || config.TelegramChatID == "" { return } if len(notifications) == 0 { return } var message string dryRunPrefix := "" if isDryRun { dryRunPrefix = "๐Ÿงช DRY RUN - " } if len(notifications) == 1 { // Single record notification notif := notifications[0] if notif.IsNew { message = fmt.Sprintf("%s๐Ÿ†• DNS Record Created\n\n"+ "Record: %s\n"+ "New IP: %s\n"+ "TTL: %d seconds", dryRunPrefix, notif.RecordName, notif.NewIP, config.TTL) } else if notif.OldIP == notif.NewIP { message = fmt.Sprintf("%s๐Ÿ”„ DNS Record Force Updated\n\n"+ "Record: %s\n"+ "IP: %s (unchanged)\n"+ "TTL: %d seconds\n"+ "Note: Forced update requested", dryRunPrefix, notif.RecordName, notif.NewIP, config.TTL) } else { message = fmt.Sprintf("%s๐Ÿ”„ IP Address Changed\n\n"+ "Record: %s\n"+ "Old IP: %s\n"+ "New IP: %s\n"+ "TTL: %d seconds", dryRunPrefix, notif.RecordName, notif.OldIP, notif.NewIP, config.TTL) } } else { // Multiple records notification var newCount, updatedCount int for _, notif := range notifications { if notif.IsNew { newCount++ } else { updatedCount++ } } message = fmt.Sprintf("%s๐Ÿ“‹ Multiple DNS Records Updated\n\n", dryRunPrefix) if newCount > 0 { message += fmt.Sprintf("๐Ÿ†• Created: %d record(s)\n", newCount) } if updatedCount > 0 { message += fmt.Sprintf("๐Ÿ”„ Updated: %d record(s)\n", updatedCount) } message += fmt.Sprintf("\nNew IP: %s\nTTL: %d seconds\n\nRecords:", notifications[0].NewIP, config.TTL) for _, notif := range notifications { if notif.IsNew { message += fmt.Sprintf("\nโ€ข %s (new)", notif.RecordName) } else if notif.OldIP == notif.NewIP { message += fmt.Sprintf("\nโ€ข %s (forced)", notif.RecordName) } else { message += fmt.Sprintf("\nโ€ข %s (%s โ†’ %s)", notif.RecordName, notif.OldIP, notif.NewIP) } } } // Send the notification using the same logic as single notifications telegramURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", config.TelegramBotToken) payload := map[string]interface{}{ "chat_id": config.TelegramChatID, "text": message, "parse_mode": "HTML", } jsonData, err := json.Marshal(payload) if err != nil { if config.Verbose { fmt.Printf("Failed to marshal Telegram payload: %v\n", err) } return } req, err := http.NewRequest("POST", telegramURL, bytes.NewBuffer(jsonData)) if err != nil { if config.Verbose { fmt.Printf("Failed to create Telegram request: %v\n", err) } return } req.Header.Set("Content-Type", "application/json") resp, err := config.Client.Do(req) if err != nil { if config.Verbose { fmt.Printf("Failed to send Telegram notification: %v\n", err) } return } defer resp.Body.Close() if resp.StatusCode == 200 { if config.Verbose { fmt.Println("Telegram notification sent successfully") } } else { if config.Verbose { body, _ := io.ReadAll(resp.Body) fmt.Printf("Telegram notification failed (HTTP %d): %s\n", resp.StatusCode, string(body)) } } }