package main import ( "io" "log" "net/http" "os" "encoding/json" "github.com/joho/godotenv" "time" "encoding/gob" "bytes" "fmt" "sync" _ "time/tzdata" ics "github.com/arran4/golang-ical" ) type prayerTimes struct { FetchedAt int64 Times map[string]string } func main() { // load the env and check for API key, but its ok if there is no .env as there is (hopefully) env variables in the envrionment itself or whatever _ = godotenv.Load() var mu sync.Mutex apiKey := os.Getenv("LPT_API_KEY") if apiKey == "" { log.Fatal("LPT_API_KEY is not set") } url := "https://www.londonprayertimes.com/api/times/?format=json&24hours=true&key=" + apiKey // times := getTimes(url, "./cache.gob") // example {1791571473 map[Asr:15:43 Dhuhr:12:53 Fajr:05:41 Isha:19:45 Maghrib:18:24 Sunrise:07:11]} loc, err := time.LoadLocation("Europe/London") time.Local = loc if err != nil { log.Fatal(err) } http.HandleFunc("/prayercal.ics", func(w http.ResponseWriter, r *http.Request) { mu.Lock() times, err := getTimes(url, "/data/cache.gob") mu.Unlock() if err != nil { log.Println(err) http.Error(w, "upstream error", http.StatusBadGateway) return } out, err := buildCal(times, loc) if err != nil { log.Println(err) http.Error(w, "bad prayer data", http.StatusBadGateway) return } w.Header().Set("Content-Type", "text/calendar; charset=utf-8") io.WriteString(w, out) }) log.Fatal(http.ListenAndServe(":5543", nil)) } func buildCal(times prayerTimes, loc *time.Location) (string, error) { cal := ics.NewCalendar() cal.SetMethod(ics.MethodPublish) for _, name := range prayerNames { start, err := parseToday(times.Times[name], loc) if err != nil { return "", fmt.Errorf("%s: %w", name, err) } ev := cal.AddEvent(name + "-" + start.Format("20060102") + "@prayercal") ev.SetCreatedTime(time.Now()) ev.SetDtStampTime(time.Now()) ev.SetModifiedAt(time.Now()) ev.SetStartAt(start) ev.SetEndAt(start.Add(20 * time.Minute)) ev.SetSummary(name) ev.SetDescription(name + " prayer time") for _, a := range []struct{ trigger, desc string }{ {"-PT15M", name + " in 15 minutes"}, {"PT0S", name + " time now"}, } { alarm := ev.AddAlarm() alarm.SetAction(ics.ActionDisplay) alarm.SetTrigger(a.trigger) alarm.SetDescription(a.desc) } } return cal.Serialize(), nil } var prayerNames = []string{"Fajr", "Sunrise", "Dhuhr", "Asr", "Maghrib", "Isha"} var httpClient = &http.Client{Timeout: 10 * time.Second} func parseToday(hhmm string, loc *time.Location) (time.Time, error) { t, err := time.Parse("15:04", hhmm) if err != nil { return time.Time{}, fmt.Errorf("bad time %q: %w", hhmm, err) } now := time.Now().In(loc) return time.Date(now.Year(), now.Month(), now.Day(), t.Hour(), t.Minute(), 0, 0, loc), nil } func readCache(path string) (prayerTimes, bool) { file, err := os.Open(path) if err != nil { if !os.IsNotExist(err) { log.Println("cache open error:", err) } return prayerTimes{}, false } defer file.Close() var cached prayerTimes if err := gob.NewDecoder(file).Decode(&cached); err != nil { log.Println("cache decode error, refetching:", err) return prayerTimes{}, false } y1, m1, d1 := time.Unix(cached.FetchedAt, 0).Date() y2, m2, d2 := time.Now().Date() return cached, y1 == y2 && m1 == m2 && d1 == d2 } func getTimes(url, cache string) (prayerTimes, error) { if cached, ok := readCache(cache); ok { log.Println("cache hit") return cached, nil } log.Println("cache miss") resp, err := httpClient.Get(url) if err != nil { return prayerTimes{}, fmt.Errorf("fetch: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return prayerTimes{}, fmt.Errorf("fetch: status %s", resp.Status) } body, err := io.ReadAll(resp.Body) if err != nil { return prayerTimes{}, fmt.Errorf("read body: %w", err) } var raw map[string]string if err := json.Unmarshal(body, &raw); err != nil { return prayerTimes{}, fmt.Errorf("unmarshal: %w", err) } form := prayerTimes{ FetchedAt: time.Now().Unix(), Times: map[string]string{ "Fajr": raw["fajr"], "Sunrise": raw["sunrise"], "Dhuhr": raw["dhuhr"], "Asr": raw["asr"], "Maghrib": raw["magrib"], "Isha": raw["isha"], }, } // don't cache a response with missing fields for _, name := range prayerNames { if form.Times[name] == "" { return prayerTimes{}, fmt.Errorf("api response missing %s", name) } } var buf bytes.Buffer if err := gob.NewEncoder(&buf).Encode(form); err != nil { log.Println("cache encode error:", err) // still serve the data return form, nil } if err := os.WriteFile(cache, buf.Bytes(), 0644); err != nil { log.Println("cache write error:", err) } return form, nil }