package main import ( "bufio" "context" "encoding/json" "fmt" "io" "log" "net" "net/http" "net/url" "os" "strings" "sync" "time" ) const ( defaultPort = "79" weatherAPIURL = "http://api.worldweatheronline.com/premium/v1/weather.ashx" readTimeout = 30 * time.Second writeTimeout = 30 * time.Second httpTimeout = 15 * time.Second forecastDays = 3 hourlyInterval = 6 separatorLength = 60 ) // Config holds server configuration type Config struct { Port string APIKey string } // Server represents the weather finger server type Server struct { config *Config client *http.Client logger *log.Logger shutdown chan struct{} wg sync.WaitGroup } // Weather API response structures type WeatherResponse struct { Data WeatherData `json:"data"` Error []WeatherError `json:"error,omitempty"` } type WeatherData struct { Request []RequestInfo `json:"request"` NearestArea []NearestArea `json:"nearest_area"` CurrentCondition []CurrentCondition `json:"current_condition"` Weather []DailyWeather `json:"weather"` } type RequestInfo struct { Query string `json:"query"` Type string `json:"type"` } type NearestArea struct { AreaName []NameValue `json:"areaName"` Country []NameValue `json:"country"` } type NameValue struct { Value string `json:"value"` } type CurrentCondition struct { TempC string `json:"temp_C"` TempF string `json:"temp_F"` WeatherDesc []NameValue `json:"weatherDesc"` Humidity string `json:"humidity"` Visibility string `json:"visibility"` Pressure string `json:"pressure"` CloudCover string `json:"cloudcover"` FeelsLikeC string `json:"FeelsLikeC"` FeelsLikeF string `json:"FeelsLikeF"` WindSpeedKmph string `json:"windspeedKmph"` WindSpeedMiles string `json:"windspeedMiles"` WindDir16Point string `json:"winddir16Point"` ObservationTime string `json:"observation_time"` } type DailyWeather struct { Date string `json:"date"` MaxTempC string `json:"maxtempC"` MaxTempF string `json:"maxtempF"` MinTempC string `json:"mintempC"` MinTempF string `json:"mintempF"` Hourly []HourlyWeather `json:"hourly"` } type HourlyWeather struct { Time string `json:"time"` TempC string `json:"tempC"` TempF string `json:"tempF"` WeatherDesc []NameValue `json:"weatherDesc"` ChanceOfRain string `json:"chanceofrain"` Humidity string `json:"humidity"` } type WeatherError struct { Msg string `json:"msg"` } func main() { config := &Config{ Port: getEnvOrDefault("WEATHER_PORT", defaultPort), APIKey: os.Getenv("WEATHER_API_KEY"), } if config.APIKey == "" { log.Fatal("WEATHER_API_KEY environment variable is not set") } server := NewServer(config) if err := server.Start(); err != nil { log.Fatalf("Server failed to start: %v", err) } } // NewServer creates a new weather finger server instance func NewServer(config *Config) *Server { return &Server{ config: config, client: &http.Client{ Timeout: httpTimeout, }, logger: log.New(os.Stdout, "[WeatherServer] ", log.LstdFlags), shutdown: make(chan struct{}), } } // Start begins listening for connections func (s *Server) Start() error { listener, err := net.Listen("tcp", ":"+s.config.Port) if err != nil { return fmt.Errorf("failed to start listener: %w", err) } defer listener.Close() s.logger.Printf("Weather Finger Server started on port %s", s.config.Port) for { select { case <-s.shutdown: s.logger.Println("Shutting down server...") s.wg.Wait() return nil default: conn, err := listener.Accept() if err != nil { s.logger.Printf("Failed to accept connection: %v", err) continue } s.wg.Add(1) go s.handleConnection(conn) } } } // Stop gracefully shuts down the server func (s *Server) Stop() { close(s.shutdown) } // handleConnection processes a single client connection func (s *Server) handleConnection(conn net.Conn) { defer s.wg.Done() defer conn.Close() if err := s.processRequest(conn); err != nil { s.logger.Printf("Error processing request: %v", err) fmt.Fprintf(conn, "Error: %v\n", err) } } // processRequest reads the query and writes the weather response func (s *Server) processRequest(conn net.Conn) error { // Set timeouts conn.SetReadDeadline(time.Now().Add(readTimeout)) conn.SetWriteDeadline(time.Now().Add(writeTimeout)) // Read request reader := bufio.NewReader(conn) request, err := reader.ReadString('\n') if err != nil { if err == io.EOF { return fmt.Errorf("client disconnected") } return fmt.Errorf("failed to read request: %w", err) } query := strings.TrimSpace(request) if query == "" { return fmt.Errorf("empty query") } clientAddr := conn.RemoteAddr().String() s.logger.Printf("Request from %s: '%s'", clientAddr, query) // Get weather data ctx, cancel := context.WithTimeout(context.Background(), httpTimeout) defer cancel() forecast, err := s.getWeatherForecast(ctx, query) if err != nil { return fmt.Errorf("failed to get weather: %w", err) } // Format and send response response := formatWeatherResponse(forecast) _, err = conn.Write([]byte(response)) return err } // getWeatherForecast fetches weather data from the API func (s *Server) getWeatherForecast(ctx context.Context, query string) (*WeatherResponse, error) { params := url.Values{ "key": {s.config.APIKey}, "q": {query}, "format": {"json"}, "num_of_days": {fmt.Sprintf("%d", forecastDays)}, "tp": {fmt.Sprintf("%d", hourlyInterval)}, "includelocation": {"yes"}, } apiURL := fmt.Sprintf("%s?%s", weatherAPIURL, params.Encode()) req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } resp, err := s.client.Do(req) if err != nil { return nil, fmt.Errorf("API request failed: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("API returned status %d", resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response: %w", err) } var weatherData WeatherResponse if err := json.Unmarshal(body, &weatherData); err != nil { return nil, fmt.Errorf("failed to parse response: %w", err) } if len(weatherData.Error) > 0 { return nil, fmt.Errorf("API error: %s", weatherData.Error[0].Msg) } return &weatherData, nil } // formatWeatherResponse creates a human-readable weather report func formatWeatherResponse(response *WeatherResponse) string { var sb strings.Builder data := &response.Data // Write header writeHeader(&sb, data) // Write current conditions writeCurrentConditions(&sb, data) // Write forecast writeForecast(&sb, data) return sb.String() } // writeHeader writes the location header func writeHeader(sb *strings.Builder, data *WeatherData) { if len(data.NearestArea) > 0 { area := data.NearestArea[0] areaName := getFirstValue(area.AreaName) countryName := getFirstValue(area.Country) sb.WriteString(fmt.Sprintf("Weather forecast for: %s, %s\n", areaName, countryName)) } else { sb.WriteString("Weather forecast\n") } sb.WriteString(strings.Repeat("=", separatorLength)) sb.WriteString("\n") } // writeCurrentConditions writes the current weather conditions func writeCurrentConditions(sb *strings.Builder, data *WeatherData) { if len(data.CurrentCondition) == 0 { sb.WriteString("CURRENT CONDITIONS: Not available\n\n") return } current := data.CurrentCondition[0] desc := getFirstValue(current.WeatherDesc) sb.WriteString("CURRENT CONDITIONS:\n") fmt.Fprintf(sb, " Temperature: %s°C (%s°F) - feels like %s°C (%s°F)\n", current.TempC, current.TempF, current.FeelsLikeC, current.FeelsLikeF) fmt.Fprintf(sb, " Conditions: %s\n", desc) fmt.Fprintf(sb, " Humidity: %s%%\n", current.Humidity) fmt.Fprintf(sb, " Wind: %s km/h %s (%s mph)\n", current.WindSpeedKmph, current.WindDir16Point, current.WindSpeedMiles) fmt.Fprintf(sb, " Pressure: %s mb\n", current.Pressure) fmt.Fprintf(sb, " Visibility: %s km\n", current.Visibility) fmt.Fprintf(sb, " Cloud Cover: %s%%\n", current.CloudCover) fmt.Fprintf(sb, " Observed: %s\n", current.ObservationTime) sb.WriteString("\n") } // writeForecast writes the daily forecast func writeForecast(sb *strings.Builder, data *WeatherData) { if len(data.Weather) == 0 { sb.WriteString("FORECAST: Not available\n") return } fmt.Fprintf(sb, "%d-DAY FORECAST:\n", len(data.Weather)) for _, day := range data.Weather { dateStr := formatDate(day.Date) fmt.Fprintf(sb, " %s:\n", dateStr) fmt.Fprintf(sb, " High: %s°C (%s°F) Low: %s°C (%s°F)\n", day.MaxTempC, day.MaxTempF, day.MinTempC, day.MinTempF) if len(day.Hourly) > 0 { sb.WriteString(" Hourly:\n") for _, hour := range day.Hourly { timeStr := formatTime(hour.Time) desc := getFirstValue(hour.WeatherDesc) fmt.Fprintf(sb, " %s: %s°C (%s°F) %s (rain: %s%%, humidity: %s%%)\n", timeStr, hour.TempC, hour.TempF, desc, hour.ChanceOfRain, hour.Humidity) } } } } // Helper functions // getFirstValue safely extracts the first value from a NameValue slice func getFirstValue(values []NameValue) string { if len(values) > 0 { return values[0].Value } return "N/A" } // formatDate formats a date string for display func formatDate(dateStr string) string { date, err := time.Parse("2006-01-02", dateStr) if err != nil { return dateStr } return date.Format("Mon, Jan 2") } // formatTime formats API time values to HH:MM format func formatTime(timeStr string) string { // Normalize to 4-digit format padded := fmt.Sprintf("%04s", timeStr) if len(padded) >= 4 { return padded[:2] + ":" + padded[2:4] } return timeStr } // getEnvOrDefault returns environment variable value or default func getEnvOrDefault(key, defaultValue string) string { if value := os.Getenv(key); value != "" { return value } return defaultValue }