Ctrl K
ComunidadGuías…

handlers.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"regexp"
	"strconv"
	"time"
	"unicode/utf8"

	"github.com/mailru/easyjson"
	"github.com/rs/zerolog/log"
	"gopkg.in/yaml.v3"
)

// Compile regex once at package level for better performance
var unicodePattern = regexp.MustCompile(`\\U([0-9A-Fa-f]{8})`)

// setCORSHeaders sets common CORS headers
func setCORSHeaders(w http.ResponseWriter) {
	w.Header().Set("Access-Control-Allow-Origin", "*")
}

// setJSONHeaders sets common headers for JSON responses
func setJSONHeaders(w http.ResponseWriter) {
	setCORSHeaders(w)
	w.Header().Set("Content-Type", "application/json")
}

// encodeJSONWithCompression streams JSON directly to the response writer.
// Note: Actual gzip compression is now handled globally by the gziphandler middleware.
func encodeJSONWithCompression(w http.ResponseWriter, r *http.Request, data interface{}) error {
	// Try to use easyjson for supported types
	if marshaler, ok := data.(easyjson.Marshaler); ok {
		_, err := easyjson.MarshalToWriter(marshaler, w)
		return err
	}
	
	// Fallback to standard json
	encoder := json.NewEncoder(w)
	return encoder.Encode(data)
}

// getResponseTimeData retrieves response time history for a monitor within a time range
func getResponseTimeData(monitorID string, timeRange string) []ResponseTimeData {
	id, err := strconv.ParseUint(monitorID, 10, 32)
	if err != nil {
		return []ResponseTimeData{}
	}

	// Calculate time cutoff based on range
	var cutoffTime time.Time
	now := time.Now()
	
	switch timeRange {
	case "1h":
		cutoffTime = now.Add(-1 * time.Hour)
	case "12h":
		cutoffTime = now.Add(-12 * time.Hour)
	case "1w":
		cutoffTime = now.Add(-7 * 24 * time.Hour)
	case "1y":
		cutoffTime = now.Add(-365 * 24 * time.Hour)
	default:
		// Default to 24 hours
		cutoffTime = now.Add(-24 * time.Hour)
	}

	// Get checks within time range, ordered by creation time
	var checks []CheckHistory
	query := db.Where("monitor_id = ? AND created_at > ?", id, cutoffTime).
		Order("created_at ASC")
	
	// Limit results based on time range to avoid too much data
	switch timeRange {
	case "1h":
		query = query.Limit(60) // Max 60 points for 1 hour
	case "12h":
		query = query.Limit(144) // Max 144 points for 12 hours
	case "24h":
		query = query.Limit(288) // Max 288 points for 24 hours
	case "1w":
		query = query.Limit(168) // Max 168 points for 1 week
	case "1y":
		query = query.Limit(365) // Max 365 points for 1 year
	default:
		query = query.Limit(50)
	}
	
	query.Find(&checks)

	// If no data, return empty array
	if len(checks) == 0 {
		return []ResponseTimeData{}
	}

	// Convert to response time data format
	// Send ISO 8601 timestamps and let the frontend format them in the user's timezone
	data := make([]ResponseTimeData, len(checks))
	for i, check := range checks {
		// Send ISO 8601 timestamp (UTC) - frontend will format in user's timezone
		isoTimestamp := check.CreatedAt.Format(time.RFC3339)
		
		// Also provide a fallback formatted string (UTC) for backwards compatibility
		var timeStr string
		switch timeRange {
		case "1h", "12h", "24h":
			timeStr = check.CreatedAt.Format("03:04 PM")
		case "1w":
			timeStr = check.CreatedAt.Format("Mon 03:04 PM")
		case "1y":
			timeStr = check.CreatedAt.Format("Jan 2")
		default:
			timeStr = check.CreatedAt.Format("03:04 PM")
		}
		
		data[i] = ResponseTimeData{
			Time:         timeStr,      // Fallback (will be overridden by frontend)
			Timestamp:    isoTimestamp, // ISO 8601 timestamp for client-side formatting
			ResponseTime: float64(check.ResponseTime),
		}
	}

	return data
}

// apiMonitors handles GET requests to list all monitors
func apiMonitors(w http.ResponseWriter, r *http.Request) {
	log.Info().Str("method", r.Method).Str("path", r.URL.Path).Msg("[API] Request")
	
	setJSONHeaders(w)

	if r.Method == http.MethodGet {
		var monitors []Monitor
		if err := db.Find(&monitors).Error; err != nil {
			log.Error().Err(err).Msg("[API] ERROR GET /api/monitors")
			http.Error(w, "Failed to fetch monitors", http.StatusInternalServerError)
			return
		}
		log.Info().Int("count", len(monitors)).Msg("[API] GET /api/monitors")
		if err := encodeJSONWithCompression(w, r, monitors); err != nil {
			log.Error().Err(err).Msg("[API] ERROR encoding monitors")
		}
		return
	}

	log.Warn().Str("method", r.Method).Msg("[API] ERROR Method not allowed")
	http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}

// apiCreateMonitor handles POST requests to create a new monitor
func apiCreateMonitor(w http.ResponseWriter, r *http.Request) {
	log.Info().Str("method", r.Method).Str("path", r.URL.Path).Msg("[API] Request")
	
	setJSONHeaders(w)
	w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
	w.Header().Set("Access-Control-Allow-Headers", "Content-Type")

	if r.Method == http.MethodOptions {
		log.Debug().Msg("[API] OPTIONS /api/monitors/create: CORS preflight")
		w.WriteHeader(http.StatusOK)
		return
	}

	if r.Method != http.MethodPost {
		log.Warn().Str("method", r.Method).Msg("[API] ERROR Method not allowed")
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	var req CreateMonitorRequest
	bodyBytes, err := io.ReadAll(r.Body)
	if err != nil {
		log.Error().Err(err).Msg("[API] ERROR POST /api/monitors/create: Failed to read body")
		http.Error(w, "Failed to read request body", http.StatusBadRequest)
		return
	}
	if err := easyjson.Unmarshal(bodyBytes, &req); err != nil {
		log.Error().Err(err).Msg("[API] ERROR POST /api/monitors/create: Invalid request body")
		http.Error(w, "Invalid request body", http.StatusBadRequest)
		return
	}

	if req.Name == "" || req.URL == "" {
		log.Warn().Str("name", req.Name).Str("url", req.URL).Msg("[API] ERROR POST /api/monitors/create: Missing required fields")
		http.Error(w, "Name and URL are required", http.StatusBadRequest)
		return
	}

	// Set default check interval to 60 seconds if not provided
	checkInterval := req.CheckInterval
	if checkInterval <= 0 {
		checkInterval = 60
	}

	monitor := Monitor{
		Name:         req.Name,
		URL:          req.URL,
		IsThirdParty: req.IsThirdParty,
		Icon:         req.Icon,
		Status:       "unknown",
		Uptime:       0,
		ResponseTime: 0,
		LastCheck:    "never",
		CheckInterval: checkInterval,
	}

	if err := db.Create(&monitor).Error; err != nil {
		log.Error().Err(err).Msg("[API] ERROR POST /api/monitors/create: Failed to create monitor")
		http.Error(w, "Failed to create monitor", http.StatusInternalServerError)
		return
	}

	log.Info().Uint("id", monitor.ID).Str("name", monitor.Name).Str("url", monitor.URL).
		Int("check_interval", monitor.CheckInterval).Msg("[API] POST /api/monitors/create: Created monitor")

	// Trigger immediate scheduler refresh to start ticker for new monitor
	go func() {
		time.Sleep(100 * time.Millisecond)
		monitorScheduler.refreshScheduler()
	}()
	
	// Immediately check the new monitor
	go checkService(&monitor)
	
	// Broadcast new monitor via SSE
	broadcastUpdate("monitor_added", monitor)
	
	// Schedule stats update (debounced)
	broadcastStatsIfChanged()

	w.WriteHeader(http.StatusCreated)
	if err := encodeJSONWithCompression(w, r, monitor); err != nil {
		log.Error().Err(err).Msg("[API] ERROR encoding monitor")
	}
}

// apiStats handles GET requests to retrieve overall statistics
func apiStats(w http.ResponseWriter, r *http.Request) {
	log.Info().Str("method", r.Method).Str("path", r.URL.Path).Msg("[API] Request")
	
	setJSONHeaders(w)

	if r.Method != http.MethodGet {
		log.Warn().Str("method", r.Method).Msg("[API] ERROR Method not allowed")
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	stats := getStats()
	log.Info().Float64("uptime", stats.OverallUptime).Int("up", stats.ServicesUp).
		Int("down", stats.ServicesDown).Int("avg_ms", stats.AvgResponseTime).Msg("[API] GET /api/stats")
	if err := encodeJSONWithCompression(w, r, stats); err != nil {
		log.Error().Err(err).Msg("[API] ERROR encoding stats")
	}
}

// apiResponseTime handles GET requests to retrieve response time history
func apiResponseTime(w http.ResponseWriter, r *http.Request) {
	monitorID := r.URL.Query().Get("id")
	timeRange := r.URL.Query().Get("range")
	if monitorID == "" {
		monitorID = "1"
	}
	if timeRange == "" {
		timeRange = "24h" // Default to 24 hours
	}
	log.Info().Str("method", r.Method).Str("path", r.URL.Path).Str("id", monitorID).Str("range", timeRange).Msg("[API] Request")
	
	setJSONHeaders(w)

	if r.Method != http.MethodGet {
		log.Warn().Str("method", r.Method).Msg("[API] ERROR Method not allowed")
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	data := getResponseTimeData(monitorID, timeRange)
	log.Info().Str("id", monitorID).Str("range", timeRange).Int("points", len(data)).Msg("[API] GET /api/response-time")
	if err := encodeJSONWithCompression(w, r, data); err != nil {
		log.Error().Err(err).Msg("[API] ERROR encoding response time data")
	}
}

// apiSSE handles Server-Sent Events connections
func apiSSE(w http.ResponseWriter, r *http.Request) {
	log.Info().Str("remote_addr", r.RemoteAddr).Str("user_agent", r.UserAgent()).Msg("[SSE] New connection request")
	
	// Set headers for SSE
	w.Header().Set("Content-Type", "text/event-stream")
	w.Header().Set("Cache-Control", "no-cache")
	w.Header().Set("Connection", "keep-alive")
	w.Header().Set("Access-Control-Allow-Origin", "*")
	w.Header().Set("Access-Control-Allow-Headers", "Cache-Control")

	// Create client
	clientID := fmt.Sprintf("%s-%d", r.RemoteAddr, time.Now().UnixNano())
	client := sseBroadcaster.addClient(clientID)
	defer func() {
		sseBroadcaster.removeClient(clientID)
		log.Debug().Str("client_id", clientID).Msg("[SSE] Cleanup completed")
	}()

	// Send initial connection message
	connectMsg := `{"type":"connected"}`
	fmt.Fprintf(w, "data: %s\n\n", connectMsg)
	if flusher, ok := w.(http.Flusher); ok {
		flusher.Flush()
		log.Debug().Str("client_id", clientID).Msg("[SSE] Sent connection confirmation")
	} else {
		log.Warn().Str("client_id", clientID).Msg("[SSE] ResponseWriter does not support flushing")
	}

	// Keep connection alive and send updates
	ticker := time.NewTicker(30 * time.Second)
	defer ticker.Stop()
	
	messageCount := 0
	keepaliveCount := 0
	startTime := time.Now()

	for {
		select {
		case message := <-client.Send:
			messageCount++
			fmt.Fprintf(w, "data: %s\n\n", string(message))
			if flusher, ok := w.(http.Flusher); ok {
				flusher.Flush()
				log.Debug().Int("message_num", messageCount).Str("client_id", clientID).
					Int("bytes", len(message)).Msg("[SSE] Sent message")
			} else {
				log.Error().Str("client_id", clientID).Msg("[SSE] ERROR: Cannot flush message")
			}
		case <-ticker.C:
			// Send keepalive
			keepaliveCount++
			fmt.Fprintf(w, ": keepalive\n\n")
			if flusher, ok := w.(http.Flusher); ok {
				flusher.Flush()
				log.Debug().Int("keepalive_num", keepaliveCount).Str("client_id", clientID).Msg("[SSE] Sent keepalive")
			}
		case <-r.Context().Done():
			duration := time.Since(startTime)
			log.Info().Str("client_id", clientID).Dur("duration", duration).
				Int("messages", messageCount).Int("keepalives", keepaliveCount).
				Msg("[SSE] Client disconnected")
			return
		}
	}
}

// apiMonitor handles GET, PUT, and DELETE requests for individual monitors
func apiMonitor(w http.ResponseWriter, r *http.Request) {
	id := r.URL.Query().Get("id")
	log.Info().Str("method", r.Method).Str("path", r.URL.Path).Str("id", id).Msg("[API] Request")
	
	setCORSHeaders(w)
	w.Header().Set("Access-Control-Allow-Methods", "GET, PUT, DELETE, OPTIONS")
	w.Header().Set("Access-Control-Allow-Headers", "Content-Type")

	if r.Method == http.MethodOptions {
		log.Debug().Msg("[API] OPTIONS /api/monitor: CORS preflight")
		w.WriteHeader(http.StatusOK)
		return
	}

	if r.Method == http.MethodGet {
		setJSONHeaders(w)
		if id == "" {
			log.Warn().Msg("[API] ERROR GET /api/monitor: Missing id parameter")
			http.Error(w, "Missing id parameter", http.StatusBadRequest)
			return
		}

		var monitor Monitor
		monitorID, err := strconv.ParseUint(id, 10, 32)
		if err != nil {
			log.Error().Err(err).Str("id", id).Msg("[API] ERROR GET /api/monitor: Invalid id parameter")
			http.Error(w, "Invalid id parameter", http.StatusBadRequest)
			return
		}

		if err := db.First(&monitor, monitorID).Error; err != nil {
			log.Warn().Str("id", id).Msg("[API] ERROR GET /api/monitor: Monitor not found")
			http.Error(w, "Monitor not found", http.StatusNotFound)
			return
		}

		log.Info().Str("id", id).Str("name", monitor.Name).Msg("[API] GET /api/monitor")
		if err := encodeJSONWithCompression(w, r, monitor); err != nil {
			log.Error().Err(err).Msg("[API] ERROR encoding monitor")
		}
		return
	}

	if r.Method == http.MethodPut {
		setJSONHeaders(w)
		if id == "" {
			log.Warn().Msg("[API] ERROR PUT /api/monitor: Missing id parameter")
			http.Error(w, "Missing id parameter", http.StatusBadRequest)
			return
		}

		monitorID, err := strconv.ParseUint(id, 10, 32)
		if err != nil {
			log.Error().Err(err).Str("id", id).Msg("[API] ERROR PUT /api/monitor: Invalid id parameter")
			http.Error(w, "Invalid id parameter", http.StatusBadRequest)
			return
		}

		var monitor Monitor
		if err := db.First(&monitor, monitorID).Error; err != nil {
			log.Warn().Str("id", id).Msg("[API] ERROR PUT /api/monitor: Monitor not found")
			http.Error(w, "Monitor not found", http.StatusNotFound)
			return
		}

		// Read body once
		bodyBytes, err := io.ReadAll(r.Body)
		if err != nil {
			log.Error().Err(err).Str("id", id).Msg("[API] ERROR PUT /api/monitor: Failed to read body")
			http.Error(w, "Failed to read request body", http.StatusBadRequest)
			return
		}

		// Check if this is a pause/unpause request (has only "paused" field)
		// Use standard json for anonymous structs
		var pauseReq struct {
			Paused *bool `json:"paused"`
		}
		if err := json.Unmarshal(bodyBytes, &pauseReq); err == nil && pauseReq.Paused != nil {
			// This is a pause/unpause request
			monitor.Paused = *pauseReq.Paused
			if err := db.Save(&monitor).Error; err != nil {
				log.Error().Err(err).Str("id", id).Msg("[API] ERROR PUT /api/monitor: Failed to update paused state")
				http.Error(w, "Failed to update paused state", http.StatusInternalServerError)
				return
			}
			log.Info().Str("id", id).Bool("paused", monitor.Paused).Msg("[API] PUT /api/monitor: Updated paused state")
			
			// Trigger immediate scheduler refresh to pick up pause state changes
			go func() {
				time.Sleep(100 * time.Millisecond)
				monitorScheduler.refreshScheduler()
			}()
			
			// Broadcast update via SSE
			broadcastUpdate("monitor_update", monitor)
			broadcastStatsIfChanged()
			
			if err := encodeJSONWithCompression(w, r, monitor); err != nil {
				log.Error().Err(err).Msg("[API] ERROR encoding monitor")
			}
			return
		}

		// Regular update request
		var req CreateMonitorRequest
		if err := easyjson.Unmarshal(bodyBytes, &req); err != nil {
			log.Error().Err(err).Str("id", id).Msg("[API] ERROR PUT /api/monitor: Invalid request body")
			http.Error(w, "Invalid request body", http.StatusBadRequest)
			return
		}

		if req.Name == "" || req.URL == "" {
			log.Warn().Str("id", id).Msg("[API] ERROR PUT /api/monitor: Missing required fields")
			http.Error(w, "Name and URL are required", http.StatusBadRequest)
			return
		}

		// Update monitor fields
		monitor.Name = req.Name
		monitor.URL = req.URL
		monitor.IsThirdParty = req.IsThirdParty
		monitor.Icon = req.Icon
		
		// Only update CheckInterval if explicitly provided (non-zero)
		// This allows updating other fields without resetting the interval
		if req.CheckInterval > 0 {
			monitor.CheckInterval = req.CheckInterval
		}

		if err := db.Save(&monitor).Error; err != nil {
			log.Error().Err(err).Str("id", id).Msg("[API] ERROR PUT /api/monitor: Failed to update monitor")
			http.Error(w, "Failed to update monitor", http.StatusInternalServerError)
			return
		}
		
		// Reload monitor from database to ensure we have the latest data
		if err := db.First(&monitor, monitorID).Error; err != nil {
			log.Error().Err(err).Str("id", id).Msg("[API] ERROR PUT /api/monitor: Failed to reload monitor")
			http.Error(w, "Failed to reload monitor", http.StatusInternalServerError)
			return
		}

		log.Info().Str("id", id).Str("name", monitor.Name).Str("url", monitor.URL).
			Int("check_interval", monitor.CheckInterval).Msg("[API] PUT /api/monitor: Updated monitor")
		
		// Trigger immediate scheduler refresh to pick up interval changes
		// Use a delay to ensure database transaction is committed
		go func() {
			// Wait longer to ensure DB write is fully committed
			time.Sleep(500 * time.Millisecond)
			log.Info().Uint64("monitor_id", monitorID).
				Int("check_interval", monitor.CheckInterval).
				Msg("[API] Triggering scheduler refresh after monitor update")
			monitorScheduler.refreshScheduler()
		}()
		
		// Broadcast update via SSE
		broadcastUpdate("monitor_update", monitor)
		broadcastStatsIfChanged()
		
		if err := encodeJSONWithCompression(w, r, monitor); err != nil {
			log.Error().Err(err).Msg("[API] ERROR encoding monitor")
		}
		return
	}

	if r.Method == http.MethodDelete {
		if id == "" {
			log.Warn().Msg("[API] ERROR DELETE /api/monitor: Missing id parameter")
			http.Error(w, "Missing id parameter", http.StatusBadRequest)
			return
		}

		monitorID, err := strconv.ParseUint(id, 10, 32)
		if err != nil {
			log.Error().Err(err).Str("id", id).Msg("[API] ERROR DELETE /api/monitor: Invalid id parameter")
			http.Error(w, "Invalid id parameter", http.StatusBadRequest)
			return
		}

		// Check if monitor exists first
		var monitor Monitor
		if err := db.First(&monitor, monitorID).Error; err != nil {
			log.Warn().Str("id", id).Msg("[API] ERROR DELETE /api/monitor: Monitor not found")
			http.Error(w, "Monitor not found", http.StatusNotFound)
			return
		}

		if err := db.Delete(&Monitor{}, monitorID).Error; err != nil {
			log.Error().Err(err).Str("id", id).Msg("[API] ERROR DELETE /api/monitor: Failed to delete")
			http.Error(w, "Failed to delete monitor", http.StatusInternalServerError)
			return
		}

		log.Info().Str("id", id).Str("name", monitor.Name).Msg("[API] DELETE /api/monitor: Successfully deleted monitor")
		
		// Broadcast deletion via SSE
		broadcastUpdate("monitor_deleted", map[string]interface{}{"id": monitorID})
		
		// Schedule stats update (debounced)
		broadcastStatsIfChanged()
		
		w.WriteHeader(http.StatusNoContent)
		return
	}

	log.Warn().Str("method", r.Method).Msg("[API] ERROR Method not allowed")
	http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}

// apiExportMonitors handles GET requests to export all monitors as YAML
func apiExportMonitors(w http.ResponseWriter, r *http.Request) {
	log.Info().Str("method", r.Method).Str("path", r.URL.Path).Msg("[API] Request")

	setCORSHeaders(w)

	if r.Method != http.MethodGet {
		log.Warn().Str("method", r.Method).Msg("[API] ERROR Method not allowed")
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	// Fetch all monitors from database
	var monitors []Monitor
	if err := db.Find(&monitors).Error; err != nil {
		log.Error().Err(err).Msg("[API] ERROR GET /api/monitors/export: Failed to fetch monitors")
		http.Error(w, "Failed to fetch monitors", http.StatusInternalServerError)
		return
	}

	// Convert monitors to YAML format
	config := ConfigFile{
		Monitors: make([]MonitorConfig, 0, len(monitors)),
	}

	for _, monitor := range monitors {
		monitorConfig := MonitorConfig{
			Name:         monitor.Name,
			URL:          monitor.URL,
			Icon:         monitor.Icon,
			CheckInterval: monitor.CheckInterval,
			IsThirdParty: monitor.IsThirdParty,
			Paused:       monitor.Paused,
		}
		config.Monitors = append(config.Monitors, monitorConfig)
	}

	// Marshal to YAML using encoder with custom options
	var buf bytes.Buffer
	encoder := yaml.NewEncoder(&buf)
	encoder.SetIndent(2) // Use 2-space indentation
	
	if err := encoder.Encode(&config); err != nil {
		log.Error().Err(err).Msg("[API] ERROR GET /api/monitors/export: Failed to marshal YAML")
		http.Error(w, "Failed to generate YAML", http.StatusInternalServerError)
		return
	}
	encoder.Close()
	
	yamlData := buf.Bytes()
	
	// Convert Unicode escape sequences back to actual emojis
	// The yaml.v3 library escapes Unicode like "\U0001F4BB" - we need to convert these back
	yamlData = convertUnicodeEscapes(yamlData)
	
	// Set headers for file download
	w.Header().Set("Content-Type", "application/x-yaml; charset=utf-8")
	w.Header().Set("Content-Disposition", "attachment; filename=monitors.yaml")
	w.Header().Set("Content-Length", fmt.Sprintf("%d", len(yamlData)))

	// Write YAML data
	if _, err := w.Write(yamlData); err != nil {
		log.Error().Err(err).Msg("[API] ERROR GET /api/monitors/export: Failed to write response")
		return
	}

	log.Info().Int("count", len(monitors)).Msg("[API] GET /api/monitors/export: Exported monitors as YAML")
}

// convertUnicodeEscapes converts YAML Unicode escape sequences like "\U0001F4BB" back to actual emojis
func convertUnicodeEscapes(data []byte) []byte {
	result := unicodePattern.ReplaceAllFunc(data, func(match []byte) []byte {
		// Extract the hex code (8 digits after \U)
		// match is "\U0001F4BB", so we skip the first 2 bytes (\U) and take the next 8
		hexStr := string(match[2:10])
		
		// Parse the hex string to a rune (Unicode code point)
		codePoint, err := strconv.ParseUint(hexStr, 16, 32)
		if err != nil {
			// If parsing fails, return the original match
			return match
		}
		
		// Convert the code point to a UTF-8 encoded string
		r := rune(codePoint)
		if !utf8.ValidRune(r) {
			return match
		}
		
		// Encode the rune to UTF-8 bytes
		utf8Bytes := make([]byte, utf8.RuneLen(r))
		utf8.EncodeRune(utf8Bytes, r)
		
		return utf8Bytes
	})
	
	return result
}