fuzzyworld

find-stone-beer / files / main.go

main.go

5.8 KB · history · blame

1package main
2
3import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "net/http"
8 "os"
9 "path/filepath"
10 "strings"
11 "time"
12
13 "github.com/jftuga/geodist"
14 "golang.org/x/exp/slices"
15)
16
17type relevantSeller struct {
18 Name string
19 Pos geodist.Coord
20 DeliveryAgeDays int
21 Message string
22}
23
24const (
25 COORDINATE_LAT_ENV_VAR = "COORDINATE_LAT"
26 COORDINATE_LON_ENV_VAR = "COORDINATE_LON"
27 RANGE_MILES_ENV_VAR = "RANGE_MILES"
28 MAX_DELIVERY_DAYS_ENV_VAR = "MAX_DELIVERY_DAYS"
29)
30
31func main() {
32 // coords for search area
33 coords, err := GetCoordsFromEnvVars()
34 if err != nil {
35 panic(err)
36 }
37
38 // load other settings from env vars
39 var rangeMiles = GetEnvVarInteger(RANGE_MILES_ENV_VAR, 15)
40 var maxDeliveryWindowDays = GetEnvVarInteger(MAX_DELIVERY_DAYS_ENV_VAR, 14)
41
42 // descriptions of the beers and packaging
43 beersInfo, err := GetBeersInfo()
44 if err != nil {
45 panic(err)
46 }
47
48 // get sellers by location
49 sellers, err := GetSellers(coords)
50 if err != nil {
51 panic(err)
52 }
53
54 // filter sellers that sell arrogant bastard
55 arrogantBastardSellers := []Seller{}
56 for _, seller := range sellers {
57 for _, brandId := range seller.AllBrands {
58 foundBrand, brand := beersInfo.FindBrandById(brandId)
59 if foundBrand {
60 if strings.Contains(strings.ToLower(brand.Name), "arrogant") {
61 arrogantBastardSellers = append(arrogantBastardSellers, seller)
62 }
63 } else {
64 panic("Could not find brand")
65 }
66 }
67 }
68
69 now := time.Now()
70 relevantSellers := []relevantSeller{}
71
72 for _, seller := range arrogantBastardSellers {
73 // quick distance check, gives us the crowd-sourced distance, which is the shortest distance between two points, so using this as a range check is a good and cheap first pass
74 dist, _, err := geodist.VincentyDistance(coords, seller.Pos)
75 if err != nil {
76 panic(err)
77 }
78 if dist > float64(rangeMiles) {
79 continue
80 }
81
82 // calculate route distance, which is the actual distance you would drive, using Bing Maps API
83 // the results are cached for the month, for each set of coordinates, so this is a stable cached operation
84 dist, err = GetDistBetweenPoints(coords, seller.Pos)
85 if err != nil {
86 panic(err)
87 }
88 if dist > float64(rangeMiles) {
89 continue
90 }
91
92 sellerInfo, err := GetSellerInfo(seller.Nid)
93 if err != nil {
94 panic(err)
95 }
96 for _, beer := range sellerInfo.Beers {
97 ok, brand := beersInfo.FindBrandById(beer.Brand)
98 if !ok {
99 panic("Could not find brand")
100 }
101 if !strings.Contains(strings.ToLower(brand.Name), "arrogant") {
102 continue
103 }
104 lastDeliveredDuration := now.Sub(beer.Delivered)
105 daysSinceDelivery := int(lastDeliveredDuration.Hours() / 24)
106 if daysSinceDelivery > maxDeliveryWindowDays {
107 continue
108 }
109 mapsLink := getGoogleMapsLink(seller.Pos)
110 replacement, hasReplacement := NameReplacements[seller.Title]
111 if hasReplacement {
112 seller.Title = replacement
113 }
114 message := fmt.Sprintf("<a href='%s'>%s (%.1fmi)</a> - %dd\n", mapsLink, seller.Title, dist, daysSinceDelivery)
115
116 relevantSellers = append(relevantSellers, relevantSeller{
117 Name: seller.Title,
118 Pos: seller.Pos,
119 DeliveryAgeDays: daysSinceDelivery,
120 Message: message,
121 })
122 }
123 }
124
125 // sort by delivery age
126 slices.SortFunc(relevantSellers, func(a, b relevantSeller) int {
127 return a.DeliveryAgeDays - b.DeliveryAgeDays
128 })
129
130 var moreInfoText = "\n<a href='https://pavelsafronov.gitlab.io/find-stone-beer/'>...continue</a>"
131 var startingText = fmt.Sprintf("Sellers on %d/%d/%d\n", now.Year(), now.Month(), now.Day())
132 var lines []string
133 for _, seller := range relevantSellers {
134 lines = append(lines, seller.Message)
135 }
136 var message = CombineUpToMaxMessageLength(startingText, moreInfoText, lines)
137 fmt.Printf("Message length: %d\n", len(message))
138 SendPushoverMessage(message)
139
140 relevantSellersJson, err := json.MarshalIndent(relevantSellers, "", " ")
141 if err != nil {
142 panic(err)
143 }
144 resultsJson := fmt.Sprintf("var results = %s;", relevantSellersJson)
145 outPath := "./public/results.js"
146 err = os.WriteFile(outPath, []byte(resultsJson), 0644)
147 if err != nil {
148 panic(err)
149 }
150}
151
152func getGoogleMapsLink(coords geodist.Coord) string {
153 // https://www.google.com/maps/search/?api=1&query=<lat>,<lng>
154 // https://www.google.com/maps/search/?api=1&query=47.665824,-122.197289
155 return fmt.Sprintf("https://www.google.com/maps/search/?api=1&query=%f,%f", coords.Lat, coords.Lon)
156}
157
158func getCachedUrl(url string, id string, cacheFileTimeFormat string) ([]byte, error) {
159 return getCachedUrlFunc(url, id, cacheFileTimeFormat, func(bytes []byte) ([]byte, error) {
160 return bytes, nil
161 })
162}
163func getCachedUrlFunc(url string, id string, cacheFileTimeFormat string, byteProcessor func([]byte) ([]byte, error)) ([]byte, error) {
164 now := time.Now()
165 nowFormatted := now.Format(cacheFileTimeFormat)
166 cacheFile := fmt.Sprintf("cache/%s-%s.txt", id, nowFormatted)
167 dir := filepath.Dir(cacheFile)
168 _, err := os.Stat(dir)
169 dirExists := err == nil || !os.IsNotExist(err)
170 if !dirExists {
171 err = os.Mkdir(dir, 0755)
172 if err != nil {
173 return nil, err
174 }
175 }
176
177 _, err = os.Stat(cacheFile)
178 fileExists := err == nil || !os.IsNotExist(err)
179 if fileExists {
180 f, err := os.Open(cacheFile)
181 if err != nil {
182 return nil, err
183 }
184 defer f.Close()
185 bytes, err := io.ReadAll(f)
186 if err != nil {
187 return nil, err
188 }
189 return bytes, nil
190 } else {
191 resp, err := http.Get(url)
192 if err != nil {
193 return nil, err
194 }
195 defer resp.Body.Close()
196
197 body, err := io.ReadAll(resp.Body)
198 if err != nil {
199 return nil, err
200 }
201
202 if resp.StatusCode != 200 {
203 return nil, fmt.Errorf("tried reaching %s, got status code %d and body %s", url, resp.StatusCode, string(body))
204 }
205
206 body, err = byteProcessor(body)
207 if err != nil {
208 return nil, err
209 }
210
211 f, err := os.Create(cacheFile)
212 if err != nil {
213 return nil, err
214 }
215 defer f.Close()
216 _, err = f.Write(body)
217 if err != nil {
218 return nil, err
219 }
220
221 return body, nil
222 }
223}