2020-09-12 17:09:23 +03:00
package v1
import (
2021-03-24 23:49:42 +03:00
"fmt"
2022-01-02 03:22:58 +03:00
"net/http"
"regexp"
"time"
2020-09-12 17:09:23 +03:00
"github.com/gorilla/mux"
2020-11-08 12:12:49 +03:00
conf "github.com/muety/wakapi/config"
2020-09-12 17:09:23 +03:00
"github.com/muety/wakapi/models"
v1 "github.com/muety/wakapi/models/compat/shields/v1"
"github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils"
2021-03-24 23:49:42 +03:00
"github.com/patrickmn/go-cache"
2020-09-12 17:09:23 +03:00
)
const (
intervalPattern = ` interval:([a-z0-9_]+) `
2022-01-06 16:45:26 +03:00
entityFilterPattern = ` (project|os|editor|language|machine|label):([_a-zA-Z0-9-\s\.]+) `
2020-09-12 17:09:23 +03:00
)
type BadgeHandler struct {
2020-11-08 12:12:49 +03:00
config * conf . Config
userSrvc services . IUserService
summarySrvc services . ISummaryService
2021-03-24 23:49:42 +03:00
cache * cache . Cache
2020-09-12 17:09:23 +03:00
}
2020-11-08 12:12:49 +03:00
func NewBadgeHandler ( summaryService services . ISummaryService , userService services . IUserService ) * BadgeHandler {
2020-09-12 17:09:23 +03:00
return & BadgeHandler {
summarySrvc : summaryService ,
userSrvc : userService ,
2021-03-24 23:49:42 +03:00
cache : cache . New ( time . Hour , time . Hour ) ,
2020-11-08 12:12:49 +03:00
config : conf . Get ( ) ,
2020-09-12 17:09:23 +03:00
}
}
2021-02-03 23:28:02 +03:00
func ( h * BadgeHandler ) RegisterRoutes ( router * mux . Router ) {
2021-02-06 22:09:08 +03:00
// no auth middleware here, handler itself resolves the user
2021-02-07 00:40:54 +03:00
r := router . PathPrefix ( "/compat/shields/v1/{user}" ) . Subrouter ( )
2021-02-03 23:28:02 +03:00
r . Methods ( http . MethodGet ) . HandlerFunc ( h . Get )
2021-01-30 12:34:52 +03:00
}
2021-02-07 13:54:07 +03:00
// @Summary Get badge data
// @Description Retrieve total time for a given entity (e.g. a project) within a given range (e.g. one week) in a format compatible with [Shields.io](https://shields.io/endpoint). Requires public data access to be allowed.
// @ID get-badge
// @Tags badges
// @Produce json
// @Param user path string true "User ID to fetch data for"
// @Param interval path string true "Interval to aggregate data for" Enums(today, yesterday, week, month, year, 7_days, last_7_days, 30_days, last_30_days, 12_months, last_12_months, any)
// @Param filter path string true "Filter to apply (e.g. 'project:wakapi' or 'language:Go')"
// @Success 200 {object} v1.BadgeData
2022-01-02 03:22:58 +03:00
// @Router /compat/shields/v1/{user}/{interval}/{filter} [get]
2021-02-03 23:28:02 +03:00
func ( h * BadgeHandler ) Get ( w http . ResponseWriter , r * http . Request ) {
2020-09-12 17:09:23 +03:00
intervalReg := regexp . MustCompile ( intervalPattern )
entityFilterReg := regexp . MustCompile ( entityFilterPattern )
var filterEntity , filterKey string
if groups := entityFilterReg . FindStringSubmatch ( r . URL . Path ) ; len ( groups ) > 2 {
filterEntity , filterKey = groups [ 1 ] , groups [ 2 ]
}
var interval = models . IntervalPast30Days
if groups := intervalReg . FindStringSubmatch ( r . URL . Path ) ; len ( groups ) > 1 {
2021-02-06 21:52:50 +03:00
if i , err := utils . ParseInterval ( groups [ 1 ] ) ; err == nil {
interval = i
}
2020-09-12 17:09:23 +03:00
}
2021-02-07 00:32:03 +03:00
requestedUserId := mux . Vars ( r ) [ "user" ]
user , err := h . userSrvc . GetUserById ( requestedUserId )
if err != nil {
w . WriteHeader ( http . StatusNotFound )
return
}
2021-04-25 15:15:18 +03:00
_ , rangeFrom , rangeTo := utils . ResolveIntervalTZ ( interval , user . TZ ( ) )
2021-06-11 17:02:28 +03:00
minStart := rangeTo . Add ( - 24 * time . Hour * time . Duration ( user . ShareDataMaxDays ) )
2021-02-07 01:23:26 +03:00
// negative value means no limit
if rangeFrom . Before ( minStart ) && user . ShareDataMaxDays >= 0 {
2021-02-07 00:32:03 +03:00
w . WriteHeader ( http . StatusForbidden )
w . Write ( [ ] byte ( "requested time range too broad" ) )
return
}
2021-06-11 17:02:28 +03:00
var permitEntity bool
2020-11-01 14:50:59 +03:00
var filters * models . Filters
2020-09-12 17:09:23 +03:00
switch filterEntity {
case "project" :
2021-06-11 17:02:28 +03:00
permitEntity = user . ShareProjects
2020-11-01 14:50:59 +03:00
filters = models . NewFiltersWith ( models . SummaryProject , filterKey )
2020-09-12 17:09:23 +03:00
case "os" :
2021-06-11 17:02:28 +03:00
permitEntity = user . ShareOSs
2020-11-01 14:50:59 +03:00
filters = models . NewFiltersWith ( models . SummaryOS , filterKey )
2020-09-12 17:09:23 +03:00
case "editor" :
2021-06-11 17:02:28 +03:00
permitEntity = user . ShareEditors
2020-11-01 14:50:59 +03:00
filters = models . NewFiltersWith ( models . SummaryEditor , filterKey )
2020-09-12 17:09:23 +03:00
case "language" :
2021-06-11 17:02:28 +03:00
permitEntity = user . ShareLanguages
2020-11-01 14:50:59 +03:00
filters = models . NewFiltersWith ( models . SummaryLanguage , filterKey )
2020-09-12 17:09:23 +03:00
case "machine" :
2021-06-11 17:02:28 +03:00
permitEntity = user . ShareMachines
2020-11-01 14:50:59 +03:00
filters = models . NewFiltersWith ( models . SummaryMachine , filterKey )
2021-06-11 21:59:34 +03:00
case "label" :
permitEntity = user . ShareLabels
filters = models . NewFiltersWith ( models . SummaryLabel , filterKey )
2022-01-02 15:39:20 +03:00
// branches are intentionally omitted here, as only relevant in combination with a project filter
2020-11-01 18:03:30 +03:00
default :
2021-06-11 17:02:28 +03:00
permitEntity = true
2020-11-01 18:03:30 +03:00
filters = & models . Filters { }
2020-09-12 17:09:23 +03:00
}
2021-06-11 17:02:28 +03:00
if ! permitEntity {
w . WriteHeader ( http . StatusForbidden )
w . Write ( [ ] byte ( "user did not opt in to share entity-specific data" ) )
return
}
2021-03-24 23:49:42 +03:00
cacheKey := fmt . Sprintf ( "%s_%v_%s_%s" , user . ID , * interval , filterEntity , filterKey )
if cacheResult , ok := h . cache . Get ( cacheKey ) ; ok {
2021-04-26 22:26:47 +03:00
utils . RespondJSON ( w , r , http . StatusOK , cacheResult . ( * v1 . BadgeData ) )
2021-03-24 23:49:42 +03:00
return
}
2021-12-26 19:02:14 +03:00
summary , err , status := h . loadUserSummary ( user , interval , filters )
2020-09-12 17:09:23 +03:00
if err != nil {
w . WriteHeader ( status )
w . Write ( [ ] byte ( err . Error ( ) ) )
return
}
2021-12-26 19:02:14 +03:00
vm := v1 . NewBadgeDataFrom ( summary )
2021-03-24 23:49:42 +03:00
h . cache . SetDefault ( cacheKey , vm )
2021-04-26 22:26:47 +03:00
utils . RespondJSON ( w , r , http . StatusOK , vm )
2020-09-12 17:09:23 +03:00
}
2021-12-26 19:02:14 +03:00
func ( h * BadgeHandler ) loadUserSummary ( user * models . User , interval * models . IntervalKey , filters * models . Filters ) ( * models . Summary , error , int ) {
2021-04-25 15:15:18 +03:00
err , from , to := utils . ResolveIntervalTZ ( interval , user . TZ ( ) )
2020-09-12 17:09:23 +03:00
if err != nil {
return nil , err , http . StatusBadRequest
}
summaryParams := & models . SummaryParams {
From : from ,
To : to ,
User : user ,
}
2020-11-07 14:01:35 +03:00
var retrieveSummary services . SummaryRetriever = h . summarySrvc . Retrieve
if summaryParams . Recompute {
retrieveSummary = h . summarySrvc . Summarize
}
2021-12-26 19:02:14 +03:00
summary , err := h . summarySrvc . Aliased (
summaryParams . From ,
summaryParams . To ,
summaryParams . User ,
retrieveSummary ,
filters ,
summaryParams . Recompute ,
)
2020-09-12 17:09:23 +03:00
if err != nil {
return nil , err , http . StatusInternalServerError
}
return summary , nil , http . StatusOK
}