diff --git a/cmd/mi/main.go b/cmd/mi/main.go index d9add87..7916b6c 100644 --- a/cmd/mi/main.go +++ b/cmd/mi/main.go @@ -11,9 +11,11 @@ import ( "github.com/facebookarchive/flagenv" _ "github.com/joho/godotenv/autoload" + r "gopkg.in/rethinkdb/rethinkdb-go.v6" "within.website/ln" "within.website/ln/ex" "within.website/mi/rethink" + "within.website/mi/switchcounter" "within.website/x/web/useragent" ) @@ -36,6 +38,9 @@ var ( // meta noPush = flag.Bool("no-push", false, "if set, don't push content") + + // switchcounter + switchFile = flag.String("switch-file", "", "if set, import switches from CSV") ) func main() { @@ -89,6 +94,28 @@ func main() { mux: mux, } mi.RegisterRoutes() + sc := switchcounter.New(session, mux) + + if *switchFile != "" { + fin, err := os.Open(*switchFile) + if err != nil { + ln.FatalErr(ctx, err) + } + + switches, err := sc.ImportCSV(fin) + if err != nil { + ln.FatalErr(ctx, err) + } + + if len(switches) == 0 { + panic("what") + } + + err = r.Table("switches").Insert(switches).Exec(session) + if err != nil { + ln.FatalErr(ctx, err) + } + } ctx, cancel := context.WithCancel(ctx) go func(cf func()) { diff --git a/cmd/mi/middleware.go b/cmd/mi/middleware.go index d8d4f33..0cf0a9a 100644 --- a/cmd/mi/middleware.go +++ b/cmd/mi/middleware.go @@ -5,6 +5,7 @@ import ( "encoding/json" "flag" "net/http" + "strings" "time" "github.com/google/uuid" @@ -75,6 +76,10 @@ func (pm PasetoMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { goto ok } + if strings.HasPrefix(r.URL.EscapedPath(), "/webhooks/") { + goto ok + } + err = pm.v2.Verify(tok, pm.pubKey, &newJsonToken, &newFooter) if err != nil { ln.Error(r.Context(), err) diff --git a/go.mod b/go.mod index 12fff03..b573d32 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( christine.website v1.3.1 github.com/McKael/madon v2.3.0+incompatible github.com/McKael/madon/v2 v2.0.0-20180929094633-c679abc985d6 + github.com/celrenheit/sandflake v0.0.0-20190410195419-50a943690bc2 github.com/dghubble/go-twitter v0.0.0-20190719072343-39e5462e111f github.com/dghubble/oauth1 v0.6.0 github.com/facebookarchive/flagenv v0.0.0-20160425205200-fcd59fca7456 diff --git a/go.sum b/go.sum index fbe9e37..ddcb131 100644 --- a/go.sum +++ b/go.sum @@ -26,6 +26,7 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/birkelund/boltdbcache v1.0.0/go.mod h1:WgJWF40tV+4K0Q7MxAPbWEIkgs4AVUB7EyKVds0EgfQ= github.com/bwmarrin/discordgo v0.20.1/go.mod h1:O9S4p+ofTFwB02em7jkpkV8M3R0/PUVOwN61zSZ0r4Q= +github.com/celrenheit/sandflake v0.0.0-20190410195419-50a943690bc2 h1:/BpnZPo/sk1vPlt62dLya5KCn7PN9ZBDrpTGlQzgUZI= github.com/celrenheit/sandflake v0.0.0-20190410195419-50a943690bc2/go.mod h1:7L8gY0+4GYeBc9TvqVuDUq7tXuM6Sj7llnt7HkVwWlQ= github.com/cenkalti/backoff v2.0.0+incompatible h1:5IIPUHhlnUZbcHQsQou5k1Tn58nJkeJL9U+ig5CHJbY= github.com/cenkalti/backoff v2.0.0+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= diff --git a/switchcounter/ingest.go b/switchcounter/ingest.go new file mode 100644 index 0000000..75d583d --- /dev/null +++ b/switchcounter/ingest.go @@ -0,0 +1,66 @@ +package switchcounter + +import ( + "encoding/csv" + "io" + "log" + "strconv" + "time" + + "github.com/celrenheit/sandflake" +) + +type Switch struct { + ID string `json:"id"` + Who string `json:"who"` + StartedAt time.Time `json:"started_at"` + EndedAt *time.Time `json:"ended_at"` + Duration time.Duration `json:"duration"` +} + +// 2020-01-04 13:42:13 UTC +const timeFormat = `2006-01-02 15:04:05 MST` + +func (s *Switches) ImportCSV(r io.Reader) ([]Switch, error) { + var result []Switch + rd := csv.NewReader(r) + rows, err := rd.ReadAll() + if err != nil { + return nil, err + } + + for _, row := range rows[1:] { + log.Printf("%v", row) + startedAtVal := row[1] + endedAtVal := row[2] + + startedAt, err := time.Parse(timeFormat, startedAtVal) + if err != nil { + return nil, err + } + + endedAt, err := time.Parse(timeFormat, endedAtVal) + if err != nil { + return nil, err + } + + lenSeconds, err := strconv.ParseInt(row[3], 10, 64) + if err != nil { + return nil, err + } + + g := sandflake.NewFixedTimeGenerator(startedAt) + id := g.Next() + sw := Switch{ + ID: id.String(), + Who: row[0], + StartedAt: startedAt, + EndedAt: &endedAt, + Duration: time.Duration(lenSeconds) * time.Second, + } + + result = append(result, sw) + } + + return result, nil +} diff --git a/switchcounter/storage.go b/switchcounter/storage.go new file mode 100644 index 0000000..c6bb07b --- /dev/null +++ b/switchcounter/storage.go @@ -0,0 +1,23 @@ +package switchcounter + +import ( + "net/http" + + r "gopkg.in/rethinkdb/rethinkdb-go.v6" +) + +type Switches struct { + session *r.Session +} + +func New(session *r.Session, mux *http.ServeMux) *Switches { + result := &Switches{ + session: session, + } + + mux.HandleFunc("/switches/", result.GetSwitches) + mux.HandleFunc("/switches/current", result.Current) + mux.HandleFunc("/switches/switch", result.RegisterSwitch) + + return result +} diff --git a/switchcounter/switch.go b/switchcounter/switch.go new file mode 100644 index 0000000..4c7fc44 --- /dev/null +++ b/switchcounter/switch.go @@ -0,0 +1,160 @@ +package switchcounter + +import ( + "context" + "encoding/json" + "io/ioutil" + "net/http" + "strconv" + "time" + + "github.com/celrenheit/sandflake" + r "gopkg.in/rethinkdb/rethinkdb-go.v6" + "within.website/ln" +) + +func (s *Switches) Get(ctx context.Context, limit, page int) ([]Switch, error) { + var result []Switch + res, err := r.Table("switches"). + OrderBy(r.Desc("started_at")). + Limit(limit). + Skip(page * limit). + Run(s.session) + if err != nil { + return nil, err + } + + err = res.All(&result) + if err != nil { + return nil, err + } + + return result, nil +} + +func (s *Switches) Switch(ctx context.Context, who string) (Switch, Switch, error) { + var lastSw Switch + var currentSw Switch + + res, err := r.Table("switches"). + OrderBy(r.Desc("started_at")). + Limit(1). + Run(s.session) + if err != nil { + return lastSw, currentSw, err + } + + err = res.One(&lastSw) + if err != nil { + return lastSw, currentSw, err + } + + now := time.Now().UTC() + lastSw.EndedAt = &now + lastSw.Duration = now.Sub(lastSw.StartedAt).Round(time.Second) + err = r.Table("switches"). + Update(lastSw). + Exec(s.session) + if err != nil { + return lastSw, currentSw, err + } + + currentSw = Switch{ + ID: sandflake.Next().String(), + Who: who, + StartedAt: now, + } + + err = r.Table("switches"). + Insert(currentSw). + Exec(s.session) + if err != nil { + return lastSw, currentSw, err + } + + return lastSw, currentSw, nil +} + +func (s *Switches) Current(rw http.ResponseWriter, req *http.Request) { + var result Switch + res, err := r.Table("switches"). + OrderBy(r.Desc("started_at")). + Limit(1). + Run(s.session) + if err != nil { + ln.Error(req.Context(), err) + http.Error(rw, "can't get data", http.StatusInternalServerError) + } + + err = res.One(&result) + if err != nil { + panic(err) + } + + switch req.Header.Get("Accept") { + case "text/plain": + http.Error(rw, result.Who, http.StatusOK) + return + default: + json.NewEncoder(rw).Encode(result) + } +} + +func (s *Switches) GetSwitches(rw http.ResponseWriter, req *http.Request) { + var ( + limit = 40 + page = 0 + ) + q := req.URL.Query() + + if val := q.Get("limit"); val != "" { + i, err := strconv.Atoi(val) + if err != nil { + http.Error(rw, "limit is bad: "+err.Error(), http.StatusBadRequest) + return + } + limit = i + } + + if val := q.Get("page"); val != "" { + i, err := strconv.Atoi(val) + if err != nil { + http.Error(rw, "page is bad: "+err.Error(), http.StatusBadRequest) + return + } + page = i + } + + switches, err := s.Get(req.Context(), limit, page) + if err != nil { + ln.Error(req.Context(), err) + http.Error(rw, "can't get data", http.StatusInternalServerError) + return + } + + json.NewEncoder(rw).Encode(switches) +} + +func (s *Switches) RegisterSwitch(rw http.ResponseWriter, req *http.Request) { + who, err := ioutil.ReadAll(req.Body) + defer req.Body.Close() + if err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + + last, current, err := s.Switch(req.Context(), string(who)) + if err != nil { + ln.Error(req.Context(), err) + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + json.NewEncoder(rw).Encode(struct { + Last Switch `json:"last"` + Current Switch `json:"current"` + }{ + Last: last, + Current: current, + }) +}