initial commit

This commit is contained in:
Jordan Del Pilar
2026-07-14 11:14:46 -07:00
commit 66a2db7be1
6 changed files with 304 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
# Created by https://www.toptal.com/developers/gitignore/api/go,linux,windows
# Edit at https://www.toptal.com/developers/gitignore?templates=go,linux,windows
### Go ###
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/go,linux,windows
# Env files
.env
# Run Files
sunset.json
sunset.txt
+50
View File
@@ -0,0 +1,50 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
type ResponseData struct {
SunsetTime time.Time `json:"sunset"`
}
func getSunsetData(lat string, lng string) (*ResponseData, error) {
u, _ := url.Parse("https://api.sunrise-sunset.org/v2")
q := u.Query()
q.Set("lat", lat)
q.Set("lng", lng)
q.Set("formatted", "0")
u.RawQuery = q.Encode()
URL := u.String()
r, err := http.Get(URL)
if err != nil {
return nil, err
}
defer r.Body.Close()
if r.StatusCode != http.StatusOK {
return nil, fmt.Errorf("bad status: %d", r.StatusCode)
}
var apiResponse struct {
SunsetTime string `json:"sunset"`
}
if err := json.NewDecoder(r.Body).Decode(&apiResponse); err != nil {
return nil, err
}
parsedTime, err := time.Parse(time.RFC3339, apiResponse.SunsetTime)
if err != nil {
return nil, err
}
return &ResponseData{SunsetTime: parsedTime}, nil
}
+86
View File
@@ -0,0 +1,86 @@
package main
import (
"encoding/json"
"fmt"
"os"
"time"
)
type Data struct {
SunsetTime time.Time `json:"sunset"`
TenMinuteWarning time.Time `json:"ten_minute_warning"`
LastUpdated time.Time `json:"last_updated"`
MorningNotifySent bool `json:"morning_notify_sent"`
TenMinuteWarningSent bool `json:"ten_minute_warning_sent"`
}
func NewData() *Data {
return &Data{
MorningNotifySent: false,
TenMinuteWarningSent: false,
}
}
func loadData(path string) (*Data, error) {
state, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return NewData(), nil
} else {
return nil, err
}
}
if len(state) == 0 {
return NewData(), nil
}
var d Data
err = json.Unmarshal(state, &d)
if err != nil {
return nil, err
}
return &d, err
}
func (d *Data) Save(path string) error {
b, err := json.Marshal(d)
if err != nil {
return err
}
tmpPath := path + ".path"
defer os.Remove(tmpPath)
if err := os.WriteFile(tmpPath, b, 0644); err != nil {
return err
}
if err := os.Rename(tmpPath, path); err != nil {
return err
}
return nil
}
func (d *Data) HandleSunsetNotifications(path string) error {
if !d.MorningNotifySent {
t := d.SunsetTime.Format("15:04")
err := SendNotification(fmt.Sprintf("Sunset is at %s", t))
if err != nil {
return fmt.Errorf("Unable to send notification | %s", err)
}
d.MorningNotifySent = true
d.Save(path)
}
if time.Now().After(d.TenMinuteWarning) && !d.TenMinuteWarningSent {
err := SendNotification("Sunset is Approaching!")
if err != nil {
return fmt.Errorf("Unable to send notification | %s", err)
}
d.TenMinuteWarningSent = true
d.Save(path)
}
return nil
}
+3
View File
@@ -0,0 +1,3 @@
module git.delpilar.net/jdelpilar/sundown
go 1.26.4
+53
View File
@@ -0,0 +1,53 @@
package main
import (
"log"
"log/slog"
"os"
"time"
)
var logger *slog.Logger
func init() {
logger = slog.New(slog.NewJSONHandler(os.Stdout, nil))
slog.SetDefault(logger)
}
func isToday(t time.Time) bool {
now := time.Now()
return t.Year() == now.Year() && t.Month() == now.Month() && t.Day() == now.Day()
}
func main() {
sunsetJSONPath := os.Getenv("SUNSET_JSON_PATH")
sunset, err := loadData(sunsetJSONPath)
if err != nil {
log.Fatalln(err)
return
}
if !isToday(sunset.LastUpdated) {
slog.Info("Sunset Data out of date. Refreshing.", "last_update", sunset.LastUpdated)
lat := os.Getenv("LAT")
lng := os.Getenv("LNG")
apiResponse, err := getSunsetData(lat, lng)
if err != nil {
slog.Error("Unable to get Sunset data from API", "err", err)
return
}
sunset = &Data{
SunsetTime: apiResponse.SunsetTime,
TenMinuteWarning: apiResponse.SunsetTime.Add(-10 * time.Minute),
LastUpdated: time.Now(),
MorningNotifySent: false,
TenMinuteWarningSent: false,
}
sunset.Save(sunsetJSONPath)
slog.Info("Sunset Data Saved", "path", sunsetJSONPath)
}
sunset.HandleSunsetNotifications(sunsetJSONPath)
}
+37
View File
@@ -0,0 +1,37 @@
package main
import (
"fmt"
"net/http"
"os"
"strings"
)
func SendNotification(m string) error {
req, err := http.NewRequest(http.MethodPost, "https://ntfy.delpilar.net/test", strings.NewReader(m))
if err != nil {
return err
}
ntfyUser := os.Getenv("NTFY_USER")
ntfyPass := os.Getenv("NTFY_PASS")
if ntfyUser == "" || ntfyPass == "" {
return fmt.Errorf("ntfy username or password not set in environment variables. Please set them and try again")
}
req.SetBasicAuth(ntfyUser, ntfyPass)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
return nil
}