-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathapi_backend.go
398 lines (324 loc) · 10.7 KB
/
api_backend.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
package main
import (
_ "embed"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
)
//go:embed www/index.htm
var index_htm_bytes string
// file path to the space api template. must follow the space api format and will be provided
// under `/v1/spaceapi`. The current open status will be updated before serving, the file is
// not touched though.
var space_api_path string
// file path to a file that contains the api token for `/v1/space/notify-open`.
// api requests will only be accepted for this path when the `auth_token` query is
// this files content.
// NOTE: Leading and trailing whitespace will be removed from the file contents.
var auth_token_path string
// file path to a file that contains the time stamp of the last sent status update.
// NOTE: This file will be updated with each successful request.
var status_db_path string
// the default http binding for the api.
var http_binding string = "127.0.0.1:8910"
// stores the timeout after when the space is considered closed
const portal_contact_timeout = 5 * time.Minute
// Wall clock time when the plenum takes place. Adjust this when
// a new time is set.
const plenum_time time.Duration = 19*time.Hour + 0*time.Minute
func main() {
err := parseCli()
if err != nil {
log.Fatal("could not parse command line: ", err)
return
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, index_htm_bytes)
})
http.HandleFunc("/v1/space", displaySpaceStatus)
http.HandleFunc("/v1/online", displayShacklesStatus)
http.HandleFunc("/v1/plena/next", displayNextPlenum)
// http.HandleFunc("/v1/plena/next?redirect - get redirected directly to the newest wiki page
http.HandleFunc("/v1/spaceapi", displaySpaceApi)
http.HandleFunc("/v1/stats/portal", displayNotImplementedYet)
http.HandleFunc("/v1/space/notify-open", handleNotifyOpen)
log.Fatal(http.ListenAndServe(http_binding, nil))
}
// parses the command line arguments and initializes the global variables.
// will return an error if anything went wrong.
func parseCli() error {
argv := os.Args
if len(argv) < 4 || len(argv) > 5 {
return errors.New("requires arguments\nusage: api <space_api_def> <api_token> <status db>")
}
space_api_path = argv[1]
auth_token_path = argv[2]
status_db_path = argv[3]
_, err := os.ReadFile(space_api_path)
if err != nil {
log.Fatal("could not open space api file")
return err
}
_, err = os.ReadFile(auth_token_path)
if err != nil {
log.Fatal("could not open auth token path")
return err
}
// initialize last time stamp from status path
init_timestamp, err := os.ReadFile(status_db_path)
if errors.Is(err, os.ErrNotExist) {
_ = defaultInitalizeStatusDb()
} else if err == nil {
intval, err := strconv.ParseInt(string(init_timestamp), 10, 64)
if err == nil {
// make sure we haven't accidently seen the alive ping yet.
last_portal_contact = time.Unix(intval, 0)
} else {
log.Fatal("failed to read status db, please make sure it's readable and contains a valid unix timestamp!")
return err
}
} else {
log.Fatal("could not open status db path")
return err
}
if len(argv) > 4 {
http_binding = argv[4]
}
return nil
}
func defaultInitalizeStatusDb() error {
// make sure we haven't accidently seen the alive ping yet.
last_portal_contact = time.Now().Add(-2 * portal_contact_timeout)
return writeStatusDb()
}
func writeStatusDb() error {
err := os.WriteFile(status_db_path, []byte(strconv.FormatInt(last_portal_contact.Unix(), 10)), 0o666)
if err != nil {
log.Fatalln("Failed to write status db: ", err)
return err
}
return nil
}
var mutex = &sync.Mutex{}
var last_portal_state_change time.Time // stores the time for the last state change
var last_portal_contact time.Time // stores the time when we've last seen the space signal itself "open"
func isShackOpen() bool {
// lock access to shared state
was_seen_shortly_ago := last_portal_contact.After(time.Now().Add(-portal_contact_timeout))
return was_seen_shortly_ago
}
func notifyShackOpen() {
mutex.Lock()
if !isShackOpen() {
last_portal_state_change = time.Now()
}
last_portal_contact = time.Now()
_ = writeStatusDb()
mutex.Unlock()
}
func getStateChangeTime() time.Time {
if isShackOpen() {
return last_portal_state_change
} else {
return last_portal_contact.Add(portal_contact_timeout)
}
}
func handleNotifyOpen(w http.ResponseWriter, r *http.Request) {
api_key, err := os.ReadFile(auth_token_path)
if err != nil {
log.Fatalln("Failed to load api auth token:", err)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintln(w, "Failed to load api auth token:")
fmt.Fprintln(w, err)
return
}
w.Header().Add("Content-Type", "text/plain")
if r.URL.Query().Get("auth_token") == strings.TrimSpace(string(api_key)) {
notifyShackOpen()
fmt.Fprint(w, "ok")
} else {
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprint(w, "invalid token")
}
}
func displayNotImplementedYet(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "text/plain")
fmt.Fprintf(w, "Not implemented yet")
}
func serveJsonString(w http.ResponseWriter, value any) {
w.Header().Add("Content-Type", "application/json; charset=utf-8")
w.Header().Add("Access-Control-Allow-Methods", "GET,HEAD,PUT,POST,DELETE")
w.Header().Add("Access-Control-Allow-Origin", "*")
json_string, err := json.Marshal(value)
if err == nil {
fmt.Fprint(w, string(json_string))
} else {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintln(w, "Failed to serialize json:")
fmt.Fprintln(w, err)
}
}
func displaySpaceStatus(w http.ResponseWriter, r *http.Request) {
type DoorOpenState struct {
Open bool `json:"open"`
}
type SlashSpaceResponse struct {
DoorState DoorOpenState `json:"doorState"`
}
response := SlashSpaceResponse{
DoorState: DoorOpenState{
Open: isShackOpen(),
},
}
serveJsonString(w, response)
}
func displayShacklesStatus(w http.ResponseWriter, r *http.Request) {
type Api struct {
Message string `json:"message"`
List []string `json:"list"`
}
response := Api{
Message: "shackles system is offline right now",
List: []string{},
}
serveJsonString(w, response)
}
func displaySpaceApi(w http.ResponseWriter, r *http.Request) {
type SpaceApi struct {
API string `json:"api"`
Space string `json:"space"`
Logo string `json:"logo"`
URL string `json:"url"`
Icon struct {
Open string `json:"open"`
Closed string `json:"closed"`
} `json:"icon"`
Location struct {
Address string `json:"address"`
Lon float64 `json:"lon"`
Lat float64 `json:"lat"`
} `json:"location"`
Contact struct {
Phone string `json:"phone"`
Twitter string `json:"twitter"`
Email string `json:"email"`
Ml string `json:"ml"`
Irc string `json:"irc"`
} `json:"contact"`
IssueReportChannels []string `json:"issue_report_channels"`
State struct {
Icon struct {
Open string `json:"open"`
Closed string `json:"closed"`
} `json:"icon"`
Open bool `json:"open"`
Lastchange int `json:"lastchange"`
} `json:"state"`
Projects []string `json:"projects"`
}
json_string, err := os.ReadFile(space_api_path)
if err != nil {
log.Fatalln("Failed to load space api data:", err)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintln(w, "Failed to load space api data:")
fmt.Fprintln(w, err)
return
}
response := SpaceApi{}
err = json.Unmarshal([]byte(json_string), &response)
if err != nil {
log.Fatalln("Failed to parse space api data:", err)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintln(w, "Failed to parse space api data:")
fmt.Fprintln(w, err)
return
}
response.State.Open = isShackOpen()
response.State.Lastchange = int(getStateChangeTime().Unix()) // TODO: This must be better documented
serveJsonString(w, response)
}
// Computes the date of the Plenum for the week `timestamp` is in.
// Returns the start of that day.
func computePlenumForWeek(timestamp time.Time) time.Time {
day := time.Date(timestamp.Year(), timestamp.Month(), timestamp.Day(), 0, 0, 0, 0, timestamp.Location())
start_of_week := day.Add(time.Duration(-24 * int64(time.Hour) * int64(day.Weekday())))
_, week := start_of_week.ISOWeek()
var weekday time.Weekday
if week%2 != 0 {
weekday = time.Thursday
} else {
weekday = time.Wednesday
}
plenum_date := start_of_week.Add(time.Duration(24 * int64(time.Hour) * int64(weekday)))
// log.Println(timestamp, " - ", day, " - ", start_of_week, " - ", plenum_date)
return plenum_date
}
func displayNextPlenum(w http.ResponseWriter, r *http.Request) {
type PlenumInfo struct {
Date time.Time `json:"date"`
FromNow string `json:"fromNow"`
URL string `json:"url"`
}
now := time.Now().Local()
plenum_date := computePlenumForWeek(now)
plenum_date_time := plenum_date.Add(plenum_time)
// If we already missed the plenum this week,
// we have to provide the date for next week.
if plenum_date_time.Before(now) {
plenum_date = computePlenumForWeek(plenum_date.Add(7 * 24 * time.Hour))
plenum_date_time = plenum_date.Add(plenum_time)
}
response := PlenumInfo{
Date: plenum_date_time,
FromNow: "soooooon",
URL: fmt.Sprintf("https://wiki.shackspace.de/plenum/%04d-%02d-%02d", plenum_date.Year(), plenum_date.Month(), plenum_date.Day()),
}
const Day = 24 * time.Hour
time_delta_abs := plenum_date_time.Sub(now)
time_delta_day := plenum_date.Sub(now)
// log.Println("time_delta_abs = ", time_delta_abs)
// log.Println("time_delta_day = ", time_delta_day)
if time_delta_day >= 7*Day {
response.FromNow = "next week"
} else if time_delta_day >= 6*Day {
response.FromNow = "in a week"
} else if time_delta_day >= 5*Day {
response.FromNow = "in 6 days"
} else if time_delta_day >= 4*Day {
response.FromNow = "in 5 days"
} else if time_delta_day >= 3*Day {
response.FromNow = "in 4 days"
} else if time_delta_day >= 2*Day {
response.FromNow = "in 3 days"
} else if time_delta_day >= 1*Day {
response.FromNow = "in 2 days"
} else if time_delta_day >= 0*Day {
response.FromNow = "tomorrow"
} else if time_delta_abs < 15*time.Minute {
response.FromNow = "in less than 15 minutes"
} else if time_delta_abs < 30*time.Minute {
response.FromNow = "in less than 30 minutes"
} else if time_delta_abs < 1*time.Hour {
response.FromNow = "in less than one hour"
} else if time_delta_abs < 3*time.Hour {
response.FromNow = "in less than three hours"
} else {
response.FromNow = "today"
}
if r.URL.Query().Has("redirect") {
w.Header().Add("Location", response.URL)
w.Header().Add("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusFound)
fmt.Fprintf(w, "Redirecting to <a href=\"%s\">%s</a>.", response.URL, response.URL)
return
}
serveJsonString(w, response)
}