initial commit
This commit is contained in:
parent
f3cf05f3ef
commit
65c47b720b
387
main.go
Normal file
387
main.go
Normal file
@ -0,0 +1,387 @@
|
||||
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
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user