diff --git a/.gitignore b/.gitignore index cfa70d0..1155752 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ public zqdgr # compiled via go prepare -assets/tailwind.css \ No newline at end of file +src/assets/tailwind.css \ No newline at end of file diff --git a/README.md b/README.md index 8a556c6..f82dcd1 100644 --- a/README.md +++ b/README.md @@ -58,8 +58,6 @@ You can then run the binary. | -------------------------------------- | ------------------------------------------------------------------------------- | -------- | ------- | | `PASSPORT_DEV_MODE` | Enables dev mode | false | false | | `PASSPORT_ENABLE_PREFORK` | Enables preforking | false | false | -| `PASSPORT_ENABLE_WEATHER` | Enables weather data, see [Weather configuration](#weather-configuration) | false | false | -| `PASSPORT_ENABLE_UPTIME` | Enables uptime data, see [Uptime configuration](#uptime-configuration) | false | false | | `PASSPORT_ADMIN_USERNAME` | The username for the admin dashboard | true | | `PASSPORT_ADMIN_PASSWORD` | The password for the admin dashboard | true | | `PASSPORT_SEARCH_PROVIDER` | The search provider to use for the search bar, without any query parameters | true | @@ -67,25 +65,25 @@ You can then run the binary. #### Weather configuration -The following only applies if you are using the OpenWeather integration. +The weather integration is optional, and will be enabled automatically if you provide an API key. The following only applies if you are using the OpenWeatherMap integration. -| Environment Variable | Description | Required | Default | -| ----------------------------- | ------------------------------------------------------------------------- | -------- | -------------- | -| `OPENWEATHER_PROVIDER` | The weather provider to use, currently only `openweathermap` is supported | true | openweathermap | -| `OPENWEATHER_API_KEY` | The OpenWeather API key | true | | -| `OPENWEATHER_TEMP_UNITS` | The temperature units to use, either `metric` or `imperial` | false | metric | -| `OPENWEATHER_LAT` | The latitude of your location | true | | -| `OPENWEATHER_LON` | The longitude of your location | true | | -| `OPENWEATHER_UPDATE_INTERVAL` | The interval in minutes to update the weather data | false | 15 | +| Environment Variable | Description | Required | Default | +| ------------------------- | ------------------------------------------------------------------------- | -------- | -------------- | +| `WEATHER_PROVIDER` | The weather provider to use, currently only `openweathermap` is supported | false | openweathermap | +| `WEATHER_API_KEY` | The OpenWeather API key | true | | +| `WEATHER_TEMP_UNITS` | The temperature units to use, either `metric` or `imperial` | false | metric | +| `WEATHER_LAT` | The latitude of your location | true | | +| `WEATHER_LON` | The longitude of your location | true | | +| `WEATHER_UPDATE_INTERVAL` | The interval in minutes to update the weather data | false | 15 | #### Uptime configuration -The following only applies if you are using the UptimeRobot integration. +The uptime integration is optional, and will be enabled automatically if you provide an API key. The following only applies if you are using the UptimeRobot integration. -| Environment Variable | Description | Required | Default | -| ----------------------------- | ------------------------------------------------- | -------- | ------- | -| `UPTIMEROBOT_API_KEY` | The UptimeRobot API key | true | | -| `UPTIMEROBOT_UPDATE_INTERVAL` | The interval in seconds to update the uptime data | false | 300 | +| Environment Variable | Description | Required | Default | +| ------------------------ | ------------------------------------------------- | -------- | ------- | +| `UPTIME_API_KEY` | The UptimeRobot API key | true | | +| `UPTIME_UPDATE_INTERVAL` | The interval in seconds to update the uptime data | false | 300 | ### Adding links and categories diff --git a/go.mod b/go.mod index c8a0bea..4ca8dd8 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.25.0 require ( github.com/HugoSmits86/nativewebp v1.2.0 github.com/caarlos0/env/v11 v11.3.1 + golang.org/x/image v0.24.0 modernc.org/sqlite v1.39.0 ) @@ -19,7 +20,6 @@ require ( github.com/tinylib/msgp v1.4.0 // indirect golang.org/x/crypto v0.42.0 // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect - golang.org/x/image v0.24.0 // indirect golang.org/x/net v0.44.0 // indirect golang.org/x/text v0.29.0 // indirect modernc.org/libc v1.66.3 // indirect @@ -41,7 +41,6 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/rivo/uniseg v0.2.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.66.0 // indirect diff --git a/go.sum b/go.sum index 374d77a..f7c827e 100644 --- a/go.sum +++ b/go.sum @@ -43,8 +43,6 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= -github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/assets/favicon.ico b/src/assets/favicon.ico similarity index 100% rename from assets/favicon.ico rename to src/assets/favicon.ico diff --git a/assets/fonts/InstrumentSans-VariableFont_wdth,wght.woff2 b/src/assets/fonts/InstrumentSans-VariableFont_wdth,wght.woff2 similarity index 100% rename from assets/fonts/InstrumentSans-VariableFont_wdth,wght.woff2 rename to src/assets/fonts/InstrumentSans-VariableFont_wdth,wght.woff2 diff --git a/assets/leaves.webp b/src/assets/leaves.webp similarity index 100% rename from assets/leaves.webp rename to src/assets/leaves.webp diff --git a/main.go b/src/main.go similarity index 81% rename from main.go rename to src/main.go index 75e6665..10de50f 100644 --- a/main.go +++ b/src/main.go @@ -6,7 +6,6 @@ import ( "bytes" "database/sql" "embed" - "encoding/json" "errors" "fmt" "image" @@ -23,7 +22,6 @@ import ( "path/filepath" "strconv" "strings" - "sync" "syscall" "time" @@ -35,8 +33,9 @@ import ( "github.com/gofiber/template/handlebars/v2" "github.com/google/uuid" "github.com/joho/godotenv" - "github.com/juls0730/passport/middleware" - "github.com/nfnt/resize" + "github.com/juls0730/passport/src/middleware" + "github.com/juls0730/passport/src/services" + "golang.org/x/image/draw" _ "modernc.org/sqlite" ) @@ -70,37 +69,15 @@ var ( insertLinkStmt *sql.Stmt ) -type WeatherProvider string - -const ( - OpenWeatherMap WeatherProvider = "openweathermap" -) - -type WeatherConfig struct { - Provider WeatherProvider `env:"OPENWEATHER_PROVIDER" envDefault:"openweathermap"` - OpenWeather struct { - APIKey string `env:"OPENWEATHER_API_KEY"` - Units string `env:"OPENWEATHER_TEMP_UNITS" envDefault:"metric"` - Lat float64 `env:"OPENWEATHER_LAT"` - Lon float64 `env:"OPENWEATHER_LON"` - } - UpdateInterval int `env:"OPENWEATHER_UPDATE_INTERVAL" envDefault:"15"` -} - -type UptimeConfig struct { - APIKey string `env:"UPTIMEROBOT_API_KEY"` - UpdateInterval int `env:"UPTIMEROBOT_UPDATE_INTERVAL" envDefault:"300"` -} - type Config struct { DevMode bool `env:"PASSPORT_DEV_MODE" envDefault:"false"` Prefork bool `env:"PASSPORT_ENABLE_PREFORK" envDefault:"false"` - WeatherEnabled bool `env:"PASSPORT_ENABLE_WEATHER" envDefault:"false"` - Weather *WeatherConfig + WeatherAPIKey string `env:"PASSPORT_WEATHER_API_KEY"` + Weather *services.WeatherConfig - UptimeEnabled bool `env:"PASSPORT_ENABLE_UPTIME" envDefault:"false"` - Uptime *UptimeConfig + UptimeAPIKey string `env:"PASSPORT_UPTIME_API_KEY"` + Uptime *services.UptimeConfig Admin struct { Username string `env:"PASSPORT_ADMIN_USERNAME"` @@ -111,6 +88,11 @@ type Config struct { URL string `env:"PASSPORT_SEARCH_PROVIDER"` Query string `env:"PASSPORT_SEARCH_PROVIDER_QUERY_PARAM" envDefault:"q"` } + + Depricated struct { + WeatherEnabled bool `env:"PASSPORT_ENABLE_WEATHER" envDefault:"false"` + UptimeEnabled bool `env:"PASSPORT_ENABLE_UPTIME" envDefault:"false"` + } } func ParseConfig() (*Config, error) { @@ -121,18 +103,46 @@ func ParseConfig() (*Config, error) { return nil, err } - if config.WeatherEnabled { - config.Weather = &WeatherConfig{} + if config.WeatherAPIKey != "" { + config.Weather = &services.WeatherConfig{ + APIKey: config.WeatherAPIKey, + } if err := env.Parse(config.Weather); err != nil { return nil, err } + } else if config.Depricated.WeatherEnabled { + slog.Warn("Your configuration file contains depricated Weather settings. Please update your configuration file!") + depricatedWeatherConfig := &services.DepricatedWeatherConfig{} + if err := env.Parse(depricatedWeatherConfig); err != nil { + return nil, err + } + + config.Weather = &services.WeatherConfig{} + config.Weather.Provider = depricatedWeatherConfig.OpenWeather.Provider + config.Weather.APIKey = depricatedWeatherConfig.OpenWeather.APIKey + config.Weather.Units = depricatedWeatherConfig.OpenWeather.Units + config.Weather.Lat = depricatedWeatherConfig.OpenWeather.Lat + config.Weather.Lon = depricatedWeatherConfig.OpenWeather.Lon + config.Weather.UpdateInterval = depricatedWeatherConfig.UpdateInterval } - if config.UptimeEnabled { - config.Uptime = &UptimeConfig{} + if config.UptimeAPIKey != "" { + config.Uptime = &services.UptimeConfig{ + APIKey: config.UptimeAPIKey, + } if err := env.Parse(config.Uptime); err != nil { return nil, err } + } else if config.Depricated.UptimeEnabled { + slog.Warn("Your configuration file contains depricated Uptime settings. Please update your configuration file!") + depricatedUptimeConfig := &services.DepricatedUptimeConfig{} + if err := env.Parse(depricatedUptimeConfig); err != nil { + return nil, err + } + + config.Uptime = &services.UptimeConfig{} + config.Uptime.APIKey = depricatedUptimeConfig.APIKey + config.Uptime.UpdateInterval = depricatedUptimeConfig.UpdateInterval } return &config, nil @@ -141,8 +151,8 @@ func ParseConfig() (*Config, error) { type App struct { *Config *CategoryManager - *WeatherCache - *UptimeManager + *services.WeatherManager + *services.UptimeManager db *sql.DB } @@ -199,242 +209,56 @@ func NewApp(dbPath string, options map[string]any) (*App, error) { return nil, err } - var weatherCache *WeatherCache - if config.WeatherEnabled { - weatherCache = NewWeatherCache(config.Weather) + var weatherCache *services.WeatherManager + if config.WeatherAPIKey != "" { + weatherCache = services.NewWeatherManager(config.Weather) } - var uptimeManager *UptimeManager - if config.UptimeEnabled { - uptimeManager = NewUptimeManager(config.Uptime) + var uptimeManager *services.UptimeManager + if config.UptimeAPIKey != "" { + uptimeManager = services.NewUptimeManager(config.Uptime) } return &App{ Config: config, - WeatherCache: weatherCache, + WeatherManager: weatherCache, CategoryManager: categoryManager, UptimeManager: uptimeManager, db: db, }, nil } -type UptimeRobotSite struct { - FriendlyName string `json:"friendly_name"` - Url string `json:"url"` - Status int `json:"status"` -} - -type UptimeManager struct { - sites []UptimeRobotSite - lastUpdate time.Time - mutex sync.RWMutex - updateChan chan struct{} - updateInterval int - apiKey string -} - -func NewUptimeManager(config *UptimeConfig) *UptimeManager { - if config.APIKey == "" { - log.Fatalln("UptimeRobot API Key is required!") - return nil +func CropToCenter(img image.Image, outputSize int) (image.Image, error) { + if img == nil { + return nil, fmt.Errorf("input image is nil") + } + if outputSize <= 0 { + return nil, fmt.Errorf("output size must be positive") } - updateInterval := config.UpdateInterval - if updateInterval < 1 { - updateInterval = 300 + srcBounds := img.Bounds() + srcWidth := srcBounds.Dx() + srcHeight := srcBounds.Dy() + + squareSide := min(srcWidth, srcHeight) + + cropX := (srcWidth - squareSide) / 2 + cropY := (srcHeight - squareSide) / 2 + + srcCropRect := image.Rect(cropX, cropY, cropX+squareSide, cropY+squareSide) + + croppedSquareImg := image.NewRGBA(image.Rect(0, 0, squareSide, squareSide)) + draw.Draw(croppedSquareImg, croppedSquareImg.Rect, img, srcCropRect.Min, draw.Src) + + if squareSide == outputSize { + return croppedSquareImg, nil } - uptimeManager := &UptimeManager{ - updateChan: make(chan struct{}), - updateInterval: updateInterval, - apiKey: config.APIKey, - sites: []UptimeRobotSite{}, - } + outputImg := image.NewRGBA(image.Rect(0, 0, outputSize, outputSize)) - go uptimeManager.updateWorker() + draw.CatmullRom.Scale(outputImg, outputImg.Rect, croppedSquareImg, croppedSquareImg.Bounds(), draw.Src, nil) - uptimeManager.updateChan <- struct{}{} - - return uptimeManager -} - -func (u *UptimeManager) getUptime() []UptimeRobotSite { - u.mutex.RLock() - defer u.mutex.RUnlock() - return u.sites -} - -func (u *UptimeManager) updateWorker() { - ticker := time.NewTicker(time.Duration(u.updateInterval) * time.Second) - defer ticker.Stop() - - for { - select { - case <-u.updateChan: - u.update() - case <-ticker.C: - u.update() - } - } -} - -type UptimeRobotResponse struct { - Monitors []UptimeRobotSite `json:"monitors"` -} - -func (u *UptimeManager) update() { - resp, err := http.Post("https://api.uptimerobot.com/v2/getMonitors?api_key="+u.apiKey, "application/json", nil) - if err != nil { - fmt.Printf("Error fetching uptime data: %v\n", err) - return - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - fmt.Printf("Error reading response: %v\n", err) - return - } - - var monitors UptimeRobotResponse - if err := json.Unmarshal(body, &monitors); err != nil { - fmt.Printf("Error parsing uptime data: %v\n", err) - return - } - - u.mutex.Lock() - u.sites = monitors.Monitors - u.lastUpdate = time.Now() - u.mutex.Unlock() -} - -type OpenWeatherResponse struct { - Weather []struct { - Name string `json:"main"` - IconId string `json:"icon"` - } `json:"weather"` - Main struct { - Temp float64 `json:"temp"` - } `json:"main"` - Code int `json:"cod"` - Message string `json:"message"` -} - -type WeatherData struct { - Temperature float64 - WeatherText string - Icon string -} - -type WeatherCache struct { - data *WeatherData - lastUpdate time.Time - mutex sync.RWMutex - updateChan chan struct{} - tempUnits string - updateInterval int - apiKey string - lat float64 - lon float64 -} - -func NewWeatherCache(config *WeatherConfig) *WeatherCache { - if config.Provider != OpenWeatherMap { - log.Fatalln("Only OpenWeatherMap is supported!") - return nil - } - - if config.OpenWeather.APIKey == "" { - log.Fatalln("An API Key required for OpenWeather!") - return nil - } - - updateInterval := config.UpdateInterval - if updateInterval < 1 { - updateInterval = 15 - } - - units := config.OpenWeather.Units - if units == "" { - units = "metric" - } - - cache := &WeatherCache{ - data: &WeatherData{}, - updateChan: make(chan struct{}), - tempUnits: units, - updateInterval: updateInterval, - apiKey: config.OpenWeather.APIKey, - lat: config.OpenWeather.Lat, - lon: config.OpenWeather.Lon, - } - - go cache.weatherWorker() - - cache.updateChan <- struct{}{} - - return cache -} - -func (c *WeatherCache) GetWeather() WeatherData { - c.mutex.RLock() - defer c.mutex.RUnlock() - return *c.data -} - -func (c *WeatherCache) weatherWorker() { - ticker := time.NewTicker(time.Duration(c.updateInterval) * time.Minute) - defer ticker.Stop() - - for { - select { - case <-c.updateChan: - c.updateWeather() - case <-ticker.C: - c.updateWeather() - } - } -} - -func (c *WeatherCache) updateWeather() { - url := fmt.Sprintf("https://api.openweathermap.org/data/2.5/weather?lat=%f&lon=%f&appid=%s&units=%s", - c.lat, c.lon, c.apiKey, c.tempUnits) - - resp, err := http.Get(url) - if err != nil { - fmt.Printf("Error fetching weather: %v\n", err) - return - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - fmt.Printf("Error reading response: %v\n", err) - return - } - - var weatherResp OpenWeatherResponse - if err := json.Unmarshal(body, &weatherResp); err != nil { - fmt.Printf("Error parsing weather data: %v\n", err) - return - } - - // if the request failed - if weatherResp.Code != 200 { - // if there is no pre-existing data in the cache - if c.data.WeatherText == "" { - log.Fatalf("Fetching the weather data failed!\n%s\n", weatherResp.Message) - } else { - return - } - } - - c.mutex.Lock() - c.data.Temperature = weatherResp.Main.Temp - c.data.WeatherText = weatherResp.Weather[0].Name - c.data.Icon = weatherResp.Weather[0].IconId - c.lastUpdate = time.Now() - c.mutex.Unlock() + return outputImg, nil } func UploadFile(file *multipart.FileHeader, fileName, contentType string, c fiber.Ctx) (string, error) { @@ -477,7 +301,12 @@ func UploadFile(file *multipart.FileHeader, fileName, contentType string, c fibe } defer outFile.Close() - resizedImg := resize.Resize(64, 0, img, resize.MitchellNetravali) + // crop slightly larger than 64px to vastly increase the quality of the image, but not increase the file size + // *too* much and so that we dont have a ton of extra file data that will never be seen by the user + resizedImg, err := CropToCenter(img, 96) + if err != nil { + return "", err + } var buf bytes.Buffer options := &nativewebp.Options{} @@ -859,8 +688,8 @@ func main() { "Categories": app.CategoryManager.GetCategories(), } - if app.Config.WeatherEnabled { - weather := app.WeatherCache.GetWeather() + if app.Config.WeatherAPIKey != "" { + weather := app.WeatherManager.GetWeather() renderData["WeatherData"] = fiber.Map{ "Temp": weather.Temperature, @@ -869,8 +698,8 @@ func main() { } } - if app.Config.UptimeEnabled { - renderData["UptimeData"] = app.UptimeManager.getUptime() + if app.Config.UptimeAPIKey != "" { + renderData["UptimeData"] = app.UptimeManager.GetUptime() } return c.Render("views/index", renderData, "layouts/main") diff --git a/middleware/admin.go b/src/middleware/admin.go similarity index 100% rename from middleware/admin.go rename to src/middleware/admin.go diff --git a/schema.sql b/src/schema.sql similarity index 100% rename from schema.sql rename to src/schema.sql diff --git a/src/services/uptimeService.go b/src/services/uptimeService.go new file mode 100644 index 0000000..155be2b --- /dev/null +++ b/src/services/uptimeService.go @@ -0,0 +1,111 @@ +package services + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "sync" + "time" +) + +type DepricatedUptimeConfig struct { + APIKey string `env:"UPTIMEROBOT_API_KEY"` + UpdateInterval int `env:"UPTIMEROBOT_UPDATE_INTERVAL" envDefault:"300"` +} + +type UptimeConfig struct { + APIKey string + UpdateInterval int `env:"UPTIME_UPDATE_INTERVAL" envDefault:"300"` +} + +type UptimeRobotSite struct { + FriendlyName string `json:"friendly_name"` + Url string `json:"url"` + Status int `json:"status"` +} + +type UptimeManager struct { + sites []UptimeRobotSite + lastUpdate time.Time + mutex sync.RWMutex + updateChan chan struct{} + updateInterval int + apiKey string +} + +func NewUptimeManager(config *UptimeConfig) *UptimeManager { + if config.APIKey == "" { + log.Fatalln("UptimeRobot API Key is required!") + return nil + } + + updateInterval := config.UpdateInterval + if updateInterval < 1 { + updateInterval = 300 + } + + uptimeManager := &UptimeManager{ + updateChan: make(chan struct{}), + updateInterval: updateInterval, + apiKey: config.APIKey, + sites: []UptimeRobotSite{}, + } + + go uptimeManager.updateWorker() + + uptimeManager.updateChan <- struct{}{} + + return uptimeManager +} + +func (u *UptimeManager) GetUptime() []UptimeRobotSite { + u.mutex.RLock() + defer u.mutex.RUnlock() + return u.sites +} + +func (u *UptimeManager) updateWorker() { + ticker := time.NewTicker(time.Duration(u.updateInterval) * time.Second) + defer ticker.Stop() + + for { + select { + case <-u.updateChan: + u.update() + case <-ticker.C: + u.update() + } + } +} + +type UptimeRobotResponse struct { + Monitors []UptimeRobotSite `json:"monitors"` +} + +func (u *UptimeManager) update() { + resp, err := http.Post("https://api.uptimerobot.com/v2/getMonitors?api_key="+u.apiKey, "application/json", nil) + if err != nil { + fmt.Printf("Error fetching uptime data: %v\n", err) + return + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Printf("Error reading response: %v\n", err) + return + } + + var monitors UptimeRobotResponse + if err := json.Unmarshal(body, &monitors); err != nil { + fmt.Printf("Error parsing uptime data: %v\n", err) + return + } + + u.mutex.Lock() + u.sites = monitors.Monitors + u.lastUpdate = time.Now() + u.mutex.Unlock() +} diff --git a/src/services/weatherService.go b/src/services/weatherService.go new file mode 100644 index 0000000..9c4933e --- /dev/null +++ b/src/services/weatherService.go @@ -0,0 +1,158 @@ +package services + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "sync" + "time" +) + +type WeatherProvider string + +const ( + OpenWeatherMap WeatherProvider = "openweathermap" +) + +type DepricatedWeatherConfig struct { + OpenWeather struct { + Provider WeatherProvider `env:"OPENWEATHER_PROVIDER" envDefault:"openweathermap"` + APIKey string `env:"OPENWEATHER_API_KEY"` + Units string `env:"OPENWEATHER_TEMP_UNITS" envDefault:"metric"` + Lat float64 `env:"OPENWEATHER_LAT"` + Lon float64 `env:"OPENWEATHER_LON"` + } + UpdateInterval int `env:"OPENWEATHER_UPDATE_INTERVAL" envDefault:"15"` +} + +type WeatherConfig struct { + Provider WeatherProvider `env:"WEATHER_PROVIDER" envDefault:"openweathermap"` + APIKey string `env:"WEATHER_API_KEY"` + Units string `env:"WEATHER_TEMP_UNITS" envDefault:"metric"` + Lat float64 `env:"WEATHER_LAT"` + Lon float64 `env:"WEATHER_LON"` + UpdateInterval int `env:"WEATHER_UPDATE_INTERVAL" envDefault:"15"` +} + +type OpenWeatherResponse struct { + Weather []struct { + Name string `json:"main"` + IconId string `json:"icon"` + } `json:"weather"` + Main struct { + Temp float64 `json:"temp"` + } `json:"main"` + Code int `json:"cod"` + Message string `json:"message"` +} + +type WeatherData struct { + Temperature float64 + WeatherText string + Icon string +} + +type WeatherManager struct { + data *WeatherData + lastUpdate time.Time + mutex sync.RWMutex + updateChan chan struct{} + config *WeatherConfig +} + +func NewWeatherManager(config *WeatherConfig) *WeatherManager { + if config.Provider != OpenWeatherMap { + log.Fatalln("Only OpenWeatherMap is supported!") + return nil + } + + if config.APIKey == "" { + log.Fatalln("An API Key required for OpenWeather!") + return nil + } + + updateInterval := config.UpdateInterval + if updateInterval < 1 { + updateInterval = 15 + } + + units := config.Units + if units == "" { + units = "metric" + } + + cache := &WeatherManager{ + data: &WeatherData{}, + updateChan: make(chan struct{}), + config: config, + } + + go cache.weatherWorker() + + cache.updateChan <- struct{}{} + + return cache +} + +func (c *WeatherManager) GetWeather() WeatherData { + c.mutex.RLock() + defer c.mutex.RUnlock() + return *c.data +} + +func (c *WeatherManager) weatherWorker() { + ticker := time.NewTicker(time.Duration(c.config.UpdateInterval) * time.Minute) + defer ticker.Stop() + + for { + select { + case <-c.updateChan: + c.updateWeather() + case <-ticker.C: + c.updateWeather() + } + } +} + +func (c *WeatherManager) updateWeather() { + url := fmt.Sprintf("https://api.openweathermap.org/data/2.5/weather?lat=%f&lon=%f&appid=%s&units=%s", + c.config.Lat, c.config.Lon, c.config.APIKey, c.config.Units) + + resp, err := http.Get(url) + if err != nil { + fmt.Printf("Error fetching weather: %v\n", err) + return + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Printf("Error reading response: %v\n", err) + return + } + + var weatherResp OpenWeatherResponse + if err := json.Unmarshal(body, &weatherResp); err != nil { + fmt.Printf("Error parsing weather data: %v\n", err) + return + } + + // if the request failed + if weatherResp.Code != 200 { + // if there is no pre-existing data in the cache + if c.data.WeatherText == "" { + log.Fatalf("Fetching the weather data failed!\n%s\n", weatherResp.Message) + } else { + return + } + } + + c.mutex.Lock() + c.data.Temperature = weatherResp.Main.Temp + c.data.WeatherText = weatherResp.Weather[0].Name + c.data.Icon = weatherResp.Weather[0].IconId + c.lastUpdate = time.Now() + c.mutex.Unlock() +} diff --git a/styles/main.css b/src/styles/main.css similarity index 100% rename from styles/main.css rename to src/styles/main.css diff --git a/templates/layouts/main.hbs b/src/templates/layouts/main.hbs similarity index 100% rename from templates/layouts/main.hbs rename to src/templates/layouts/main.hbs diff --git a/templates/views/admin/index.hbs b/src/templates/views/admin/index.hbs similarity index 100% rename from templates/views/admin/index.hbs rename to src/templates/views/admin/index.hbs diff --git a/templates/views/admin/login.hbs b/src/templates/views/admin/login.hbs similarity index 100% rename from templates/views/admin/login.hbs rename to src/templates/views/admin/login.hbs diff --git a/templates/views/index.hbs b/src/templates/views/index.hbs similarity index 100% rename from templates/views/index.hbs rename to src/templates/views/index.hbs diff --git a/zqdgr.config.json b/zqdgr.config.json index 63558c3..54f9f39 100644 --- a/zqdgr.config.json +++ b/zqdgr.config.json @@ -10,9 +10,9 @@ "url": "https://github.com/juls0730/passport.git" }, "scripts": { - "dev": "go generate; PASSPORT_DEV_MODE=true go run main.go", - "build": "go generate && go build -tags netgo,prod -ldflags=\"-w -s\" -o passport" + "dev": "go generate ./src/; PASSPORT_DEV_MODE=true go run src/main.go", + "build": "go generate ./src/ && go build -tags netgo,prod -ldflags=\"-w -s\" -o passport" }, - "pattern": "**/*.go,templates/**/*.hbs,styles/**/*.css,assets/**/*.{svg,png,jpg,jpeg,webp,woff2,ttf,otf,eot,ico,gif,webp}", + "pattern": "src/**/*.{go,hbs,css,svg,png,jpg,jpeg,webp,woff2,ico,webp}", "shutdown_signal": "SIGINT" } \ No newline at end of file