diff --git a/socketserver/cmd/statsweb/config.go b/socketserver/cmd/statsweb/config.go index ae882d38..6ddca585 100644 --- a/socketserver/cmd/statsweb/config.go +++ b/socketserver/cmd/statsweb/config.go @@ -1,9 +1,9 @@ package main import ( - "os" - "fmt" "encoding/json" + "fmt" + "os" ) type ConfigFile struct { @@ -71,4 +71,4 @@ func loadConfig() { return } return -} \ No newline at end of file +} diff --git a/socketserver/cmd/statsweb/html.go b/socketserver/cmd/statsweb/html.go index 5634c1f1..303c4ddf 100644 --- a/socketserver/cmd/statsweb/html.go +++ b/socketserver/cmd/statsweb/html.go @@ -1,10 +1,10 @@ package main import ( + "bitbucket.org/stendec/frankerfacez/socketserver/server" "html/template" "net/http" "time" - "bitbucket.org/stendec/frankerfacez/socketserver/server" ) type CalendarData struct { @@ -14,13 +14,13 @@ type CalWeekData struct { Days []CalDayData } type CalDayData struct { - NoData bool - Date int + NoData bool + Date int UniqUsers int } type CalendarMonthInfo struct { - Year int + Year int Month time.Month // Ranges from -5 to +1. // A value of +1 means the 1st of the month is a Sunday. @@ -44,10 +44,10 @@ func GetMonthInfo(at time.Time) CalendarMonthInfo { } return CalendarMonthInfo{ - Year: year, - Month: month, + Year: year, + Month: month, FirstSundayOffset: monthWeekStartDay, - NeedSixSundays: needSixSundays, + NeedSixSundays: needSixSundays, } } diff --git a/socketserver/cmd/statsweb/servers.go b/socketserver/cmd/statsweb/servers.go new file mode 100644 index 00000000..83f9bffc --- /dev/null +++ b/socketserver/cmd/statsweb/servers.go @@ -0,0 +1,70 @@ +package main + +type serverFilter struct { + // Mode is false for blacklist, true for whitelist + Mode bool + Special string[] +} + +const serverFilterModeBlacklist = false +const serverFilterModeWhitelist = true + +func (sf *serverFilter) IsServerAllowed(server string) { + for _, v := range sf.Special { + if server == v { + return sf.Mode + } + } + return !sf.Mode +} + +func (sf *serverFilter) Remove(server string) { + if sf.Mode == serverFilterModeWhitelist { + var idx int = -1 + for i, v := range sf.Special { + if server == v { + idx = i + break + } + } + if idx != -1 { + var lenMinusOne = len(sf.Special)-1 + sf.Special[idx] = sf.Special[lenMinusOne] + sf.Special = sf.Special[:lenMinusOne] + } + } else { + for _, v := range sf.Special { + if server == v { + return + } + } + sf.Special = append(sf.Special, server) + } +} + +func (sf *serverFilter) Add(server string) { + if sf.Mode == serverFilterModeBlacklist { + var idx int = -1 + for i, v := range sf.Special { + if server == v { + idx = i + break + } + } + if idx != -1 { + var lenMinusOne = len(sf.Special)-1 + sf.Special[idx] = sf.Special[lenMinusOne] + sf.Special = sf.Special[:lenMinusOne] + } + } else { + for _, v := range sf.Special { + if server == v { + return + } + } + sf.Special = append(sf.Special, server) + } +} + +const serverFilterAll serverFilter = serverFilter{Mode: serverFilterModeBlacklist} +const serverFilterNone serverFilter = serverFilter{Mode: serverFilterModeWhitelist} diff --git a/socketserver/cmd/statsweb/statsweb.go b/socketserver/cmd/statsweb/statsweb.go index 6944be6f..284eb1d5 100644 --- a/socketserver/cmd/statsweb/statsweb.go +++ b/socketserver/cmd/statsweb/statsweb.go @@ -30,13 +30,14 @@ func main() { loadConfig() - http.HandleFunc("/api", ServeAPI) + http.HandleFunc("/api/get", ServeAPIGet) http.ListenAndServe(config.ListenAddr, http.DefaultServeMux) } const RequestURIName = "q" const separatorRange = "~" const separatorAdd = " " +const separatorServer = "@" const jsonErrMalformedRequest = `{"status":"error","error":"malformed request uri"}` const jsonErrBlankRequest = `{"status":"error","error":"no queries given"}` const statusError = "error" @@ -53,15 +54,7 @@ type requestResponse struct { Count uint64 `json:"count,omitempty"` } -type serverFilter struct { - // TODO -} -func (sf *serverFilter) IsServerAllowed(server string) { - return true -} -const serverFilterAll serverFilter - -func ServeAPI(w http.ResponseWriter, r *http.Request) { +func ServeAPIGet(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") u, err := url.ParseRequestURI(r.RequestURI) @@ -82,7 +75,10 @@ func ServeAPI(w http.ResponseWriter, r *http.Request) { resp := apiResponse{Status: statusOk} resp.Responses = make([]requestResponse, reqCount) for i, v := range query[RequestURIName] { - resp.Responses[i] = processSingleRequest(v) + if len(v) == 0 { + continue + } + resp.Responses[i] = ProcessSingleGetRequest(v) } for _, v := range resp.Responses { if v.Status == statusError { @@ -96,20 +92,61 @@ func ServeAPI(w http.ResponseWriter, r *http.Request) { enc.Encode(resp) } -const errRangeFormatIncorrect = "incorrect range format, must be yyyy-mm-dd~yyyy-mm-dd" +const errRangeFormatIncorrect = errors.New("incorrect range format, must be yyyy-mm-dd~yyyy-mm-dd") -func processSingleRequest(req string) (result requestResponse) { - // Forms: - // Single: 2016-01-02 - // Range: 2016-01-03~2016-01-09 - // Add disparate: 2016-01-02 2016-01-03 2016-01-09 2016-01-10 - // NOTE: Spaces are uri-encoded as + - // Add ranges: 2016-01-04~2016-01-08 2016-01-11~2016-01-15 +// ProcessSingleGetRequest takes a request string and pulls the unique user data for the given dates and filters. +// +// The request string is in the following format: +// +// Request = AddDateRanges [ "@" ServerFilter ] . +// ServerFilter = [ "!" ] ServerName { " " ServerName } . +// ServerName = { "a" … "z" } . +// AddDateRanges = DateMaybeRange { " " DateMaybeRange } . +// DateMaybeRange = DateRange | Date . +// DateRange = Date "~" Date . +// Date = Year "-" Month "-" Day . +// Year = number number number number . +// Month = number number . +// Day = number number . +// number = "0" … "9" . +// +// Example of a well-formed request: +// +// 2016-01-04~2016-01-08 2016-01-11~2016-01-15@andknuckles tuturu +// +// Remember that spaces are urlencoded as "+", so the HTTP request to send to retrieve that data would be this: +// +// /api/get?q=2016-01-04~2016-01-08+2016-01-11~2016-01-15%40andknuckles+tuturu +// +// If a ServerFilter is specified, only users connecting to the specified servers will be included in the count. +// +// It does not matter if a date is specified multiple times, due to the data format used. +func ProcessSingleGetRequest(req string) (result requestResponse) { var hll hyperloglog.HyperLogLogPlus, _ = hyperloglog.NewPlus(server.CounterPrecision) - addSplit := strings.Split(req, separatorAdd) result.Request = req result.Status = statusOk + filter := serverFilterAll + + collectError := func(err error) bool { + if err != nil { + result.Status = statusError + result.Error = err.Error() + return true + } + return false + } + + serverSplit := strings.Split(req, separatorServer) + if len(serverSplit) == 2 { + filter = serverFilterNone + serversOnly := strings.Split(serverSplit[1], separatorAdd) + for _, v := range serversOnly { + filter.Add(v) + } + } + + addSplit := strings.Split(serverSplit[0], separatorAdd) outerLoop: for _, split1 := range addSplit { @@ -119,40 +156,31 @@ func processSingleRequest(req string) (result requestResponse) { rangeSplit := strings.Split(split1, separatorRange) if len(rangeSplit) == 1 { - at, err := parseDate(rangeSplit[0]) - if err != nil { - result.Status = statusError - result.Error = err.Error() + at, err := parseDateFromRequest(rangeSplit[0]) + if collectError(err) { break outerLoop } - err = addSingleDate(at, serverFilterAll, &hll) - if err != nil { - result.Status = statusError - result.Error = err.Error() + + err = addSingleDate(at, filter, &hll) + if collectError(err) { break outerLoop } } else if len(rangeSplit) == 2 { - from, err := parseDate(rangeSplit[0]) - if err != nil { - result.Status = statusError - result.Error = err.Error() + from, err := parseDateFromRequest(rangeSplit[0]) + if collectError(err) { break outerLoop } - to, err := parseDate(rangeSplit[1]) - if err != nil { - result.Status = statusError - result.Error = err.Error() + to, err := parseDateFromRequest(rangeSplit[1]) + if collectError(err) { break outerLoop } - err = addRange(from, to, serverFilterAll, &hll) - if err != nil { - result.Status = statusError - result.Error = err.Error() + + err = addRange(from, to, filter, &hll) + if collectError(err) { break outerLoop } } else { - result.Status = statusError - result.Error = errRangeFormatIncorrect + collectError(errRangeFormatIncorrect) break outerLoop } } @@ -166,7 +194,7 @@ func processSingleRequest(req string) (result requestResponse) { var errBadDate = errors.New("bad date format, must be yyyy-mm-dd") var zeroTime = time.Unix(0, 0) -func parseDate(dateStr string) (time.Time, error) { +func parseDateFromRequest(dateStr string) (time.Time, error) { var year, month, day int n, err := fmt.Sscanf(dateStr, "%d-%d-%d", &year, &month, &day) if err != nil || n != 3 { @@ -182,6 +210,7 @@ func addSingleDate(at time.Time, filter serverFilter, dest *hyperloglog.HyperLog func addRange(start time.Time, end time.Time, filter serverFilter, dest *hyperloglog.HyperLogLogPlus) error { + return nil } func combineDateRange(from time.Time, to time.Time, dest *hyperloglog.HyperLogLogPlus) error {