Skip to content
This repository has been archived by the owner on Dec 7, 2023. It is now read-only.

Live leaderboard #528

Open
wants to merge 7 commits into
base: staging
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions documentation/docs/reference/services/Profile.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,47 @@ Request requires no body.
}
```

WS /profile/live/leaderboard/?limit=
-------------------------

!!! note
This is a public endpoint

This endpoint expects a websocket connection.

Returns a list of profiles sorted by points descending. If a `limit` parameter is provided, it will
return the first `limit` profiles. Otherwise, it will return all of the profiles.

Once, the connection has been established, `limit` can be easily changed by simply sending the key
in a JSON.

The API will send back a list of profiles whenever a user gets awarded points (ratelimited to 1
response per second) or if 5 minutes elapse with no activity (i.e. no point values change). The
latter is more or less a failsafe in order to ensure the leaderboard is the most up-to-date.

```json title="Exmaple WS message request"
{
"limit": 20
}
```

```json title="Example WS response"
{
"profiles": [
{
"id": "profileid123456",
"points": 2021,
"discord": "patrick#1234"
},
{
"id": "profileid123456",
"points": 2021,
"discord": "patrick#1234"
},
]
}
```

GET /profile/search/?teamStatus=value&interests=value,value,value&limit=value
-------------------------

Expand Down
58 changes: 46 additions & 12 deletions gateway/services/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,73 +17,101 @@ var ProfileRoutes = arbor.RouteCollection{
"GetCurrentUserProfile",
"GET",
"/profile/",
alice.New(middleware.IdentificationMiddleware, middleware.AuthMiddleware([]authtoken.Role{authtoken.AdminRole, authtoken.AttendeeRole, authtoken.ApplicantRole, authtoken.StaffRole, authtoken.MentorRole})).ThenFunc(GetProfile).ServeHTTP,
alice.New(middleware.IdentificationMiddleware, middleware.AuthMiddleware([]authtoken.Role{authtoken.AdminRole, authtoken.AttendeeRole, authtoken.ApplicantRole, authtoken.StaffRole, authtoken.MentorRole})).
ThenFunc(GetProfile).
ServeHTTP,
},
arbor.Route{
"CreateCurrentUserProfile",
"POST",
"/profile/",
alice.New(middleware.IdentificationMiddleware, middleware.AuthMiddleware([]authtoken.Role{authtoken.AdminRole, authtoken.AttendeeRole, authtoken.ApplicantRole, authtoken.StaffRole, authtoken.MentorRole})).ThenFunc(CreateProfile).ServeHTTP,
alice.New(middleware.IdentificationMiddleware, middleware.AuthMiddleware([]authtoken.Role{authtoken.AdminRole, authtoken.AttendeeRole, authtoken.ApplicantRole, authtoken.StaffRole, authtoken.MentorRole})).
ThenFunc(CreateProfile).
ServeHTTP,
},
arbor.Route{
"UpdateCurrentUserProfile",
"PUT",
"/profile/",
alice.New(middleware.AuthMiddleware([]authtoken.Role{authtoken.AdminRole, authtoken.AttendeeRole, authtoken.ApplicantRole, authtoken.StaffRole, authtoken.MentorRole}), middleware.IdentificationMiddleware).ThenFunc(UpdateProfile).ServeHTTP,
alice.New(middleware.AuthMiddleware([]authtoken.Role{authtoken.AdminRole, authtoken.AttendeeRole, authtoken.ApplicantRole, authtoken.StaffRole, authtoken.MentorRole}), middleware.IdentificationMiddleware).
ThenFunc(UpdateProfile).
ServeHTTP,
},
arbor.Route{
"DeleteCurrentUserProfile",
"DELETE",
"/profile/",
alice.New(middleware.AuthMiddleware([]authtoken.Role{authtoken.AdminRole}), middleware.IdentificationMiddleware).ThenFunc(DeleteProfile).ServeHTTP,
alice.New(middleware.AuthMiddleware([]authtoken.Role{authtoken.AdminRole}), middleware.IdentificationMiddleware).
ThenFunc(DeleteProfile).
ServeHTTP,
},
arbor.Route{
"GetAllProfiles",
"GET",
"/profile/list/",
alice.New(middleware.AuthMiddleware([]authtoken.Role{authtoken.AdminRole, authtoken.StaffRole}), middleware.IdentificationMiddleware).ThenFunc(GetFilteredProfiles).ServeHTTP,
alice.New(middleware.AuthMiddleware([]authtoken.Role{authtoken.AdminRole, authtoken.StaffRole}), middleware.IdentificationMiddleware).
ThenFunc(GetFilteredProfiles).
ServeHTTP,
},
arbor.Route{
"GetProfileLeaderboard",
"GET",
"/profile/leaderboard/",
alice.New(middleware.IdentificationMiddleware).ThenFunc(GetProfileLeaderboard).ServeHTTP,
},
arbor.Route{
"WSLiveProfileLeaderboard",
"GET",
"/profile/live/leaderboard/",
alice.New(middleware.IdentificationMiddleware).ThenFunc(GetLiveProfileLeaderboard).ServeHTTP,
},
arbor.Route{
"GetValidFilteredProfiles",
"GET",
"/profile/search/",
alice.New(middleware.AuthMiddleware([]authtoken.Role{authtoken.AdminRole, authtoken.AttendeeRole, authtoken.ApplicantRole, authtoken.StaffRole, authtoken.MentorRole}), middleware.IdentificationMiddleware).ThenFunc(GetValidFilteredProfiles).ServeHTTP,
alice.New(middleware.AuthMiddleware([]authtoken.Role{authtoken.AdminRole, authtoken.AttendeeRole, authtoken.ApplicantRole, authtoken.StaffRole, authtoken.MentorRole}), middleware.IdentificationMiddleware).
ThenFunc(GetValidFilteredProfiles).
ServeHTTP,
},
arbor.Route{
"RedeemEvent",
"POST",
"/profile/event/checkin/",
alice.New(middleware.AuthMiddleware([]authtoken.Role{authtoken.AdminRole, authtoken.StaffRole}), middleware.IdentificationMiddleware).ThenFunc(RedeemEvent).ServeHTTP,
alice.New(middleware.AuthMiddleware([]authtoken.Role{authtoken.AdminRole, authtoken.StaffRole}), middleware.IdentificationMiddleware).
ThenFunc(RedeemEvent).
ServeHTTP,
},
arbor.Route{
"AwardPoints",
"POST",
"/profile/points/award/",
alice.New(middleware.AuthMiddleware([]authtoken.Role{authtoken.AdminRole, authtoken.StaffRole}), middleware.IdentificationMiddleware).ThenFunc(AwardPoints).ServeHTTP,
alice.New(middleware.AuthMiddleware([]authtoken.Role{authtoken.AdminRole, authtoken.StaffRole}), middleware.IdentificationMiddleware).
ThenFunc(AwardPoints).
ServeHTTP,
},
arbor.Route{
"GetProfileFavorites",
"GET",
"/profile/favorite/",
alice.New(middleware.AuthMiddleware([]authtoken.Role{authtoken.AdminRole, authtoken.AttendeeRole, authtoken.ApplicantRole, authtoken.StaffRole, authtoken.MentorRole}), middleware.IdentificationMiddleware).ThenFunc(GetProfileFavorites).ServeHTTP,
alice.New(middleware.AuthMiddleware([]authtoken.Role{authtoken.AdminRole, authtoken.AttendeeRole, authtoken.ApplicantRole, authtoken.StaffRole, authtoken.MentorRole}), middleware.IdentificationMiddleware).
ThenFunc(GetProfileFavorites).
ServeHTTP,
},
arbor.Route{
"AddProfileFavorite",
"POST",
"/profile/favorite/",
alice.New(middleware.AuthMiddleware([]authtoken.Role{authtoken.AdminRole, authtoken.AttendeeRole, authtoken.ApplicantRole, authtoken.StaffRole, authtoken.MentorRole}), middleware.IdentificationMiddleware).ThenFunc(AddProfileFavorite).ServeHTTP,
alice.New(middleware.AuthMiddleware([]authtoken.Role{authtoken.AdminRole, authtoken.AttendeeRole, authtoken.ApplicantRole, authtoken.StaffRole, authtoken.MentorRole}), middleware.IdentificationMiddleware).
ThenFunc(AddProfileFavorite).
ServeHTTP,
},
arbor.Route{
"RemoveProfileFavorite",
"DELETE",
"/profile/favorite/",
alice.New(middleware.AuthMiddleware([]authtoken.Role{authtoken.AdminRole, authtoken.AttendeeRole, authtoken.ApplicantRole, authtoken.StaffRole, authtoken.MentorRole}), middleware.IdentificationMiddleware).ThenFunc(RemoveProfileFavorite).ServeHTTP,
alice.New(middleware.AuthMiddleware([]authtoken.Role{authtoken.AdminRole, authtoken.AttendeeRole, authtoken.ApplicantRole, authtoken.StaffRole, authtoken.MentorRole}), middleware.IdentificationMiddleware).
ThenFunc(RemoveProfileFavorite).
ServeHTTP,
},
arbor.Route{
"GetTierThresholds",
Expand All @@ -96,7 +124,9 @@ var ProfileRoutes = arbor.RouteCollection{
"GetUserProfileById",
"GET",
"/profile/{id}/",
alice.New(middleware.AuthMiddleware([]authtoken.Role{authtoken.AdminRole, authtoken.AttendeeRole, authtoken.ApplicantRole, authtoken.StaffRole, authtoken.MentorRole}), middleware.IdentificationMiddleware).ThenFunc(GetProfileById).ServeHTTP,
alice.New(middleware.AuthMiddleware([]authtoken.Role{authtoken.AdminRole, authtoken.AttendeeRole, authtoken.ApplicantRole, authtoken.StaffRole, authtoken.MentorRole}), middleware.IdentificationMiddleware).
ThenFunc(GetProfileById).
ServeHTTP,
},
}

Expand Down Expand Up @@ -132,6 +162,10 @@ func GetProfileLeaderboard(w http.ResponseWriter, r *http.Request) {
arbor.GET(w, config.PROFILE_SERVICE+r.URL.String(), ProfileFormat, "", r)
}

func GetLiveProfileLeaderboard(w http.ResponseWriter, r *http.Request) {
arbor.ProxyWebsocket(w, config.PROFILE_SERVICE+r.URL.String(), ProfileFormat, "", r)
}

func RedeemEvent(w http.ResponseWriter, r *http.Request) {
arbor.POST(w, config.PROFILE_SERVICE+r.URL.String(), ProfileFormat, "", r)
}
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/HackIllinois/api

replace github.com/arbor-dev/arbor => github.com/HackIllinois/arbor v0.3.1-0.20220625214746-96b56633f2e3
replace github.com/arbor-dev/arbor => github.com/HackIllinois/arbor v0.3.1-0.20230201230413-b6f79f9e5736

require (
github.com/arbor-dev/arbor v0.3.0
Expand All @@ -9,6 +9,7 @@ require (
github.com/go-playground/validator/v10 v10.10.0
github.com/golang-jwt/jwt/v4 v4.4.3
github.com/gorilla/mux v1.6.2
github.com/gorilla/websocket v1.5.0
github.com/justinas/alice v0.0.0-20171023064455-03f45bd4b7da
github.com/levigross/grequests v0.0.0-20181123014746-f3f67e7783bb
github.com/prometheus/client_golang v1.12.1
Expand All @@ -26,7 +27,6 @@ require (
github.com/golang/snappy v0.0.1 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/jmespath/go-jmespath v0.3.0 // indirect
github.com/kennygrant/sanitize v1.2.3 // indirect
github.com/klauspost/compress v1.13.6 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/HackIllinois/arbor v0.3.1-0.20220625214746-96b56633f2e3 h1:6OwmTZCWPOFEjRilwzMMD++PwjJfR9+hG3EXWR/EEAc=
github.com/HackIllinois/arbor v0.3.1-0.20220625214746-96b56633f2e3/go.mod h1:CilbmeWfrwvOtRRxg4oQs5NpGtklqUWCQvW9rxw+Xfk=
github.com/HackIllinois/arbor v0.3.1-0.20230201230413-b6f79f9e5736 h1:qtfomoIx6ggEIOR5U5zinYMu1ZsueqNYHBRd6pd7ttA=
github.com/HackIllinois/arbor v0.3.1-0.20230201230413-b6f79f9e5736/go.mod h1:CilbmeWfrwvOtRRxg4oQs5NpGtklqUWCQvW9rxw+Xfk=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
Expand Down
30 changes: 30 additions & 0 deletions services/profile/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package controller
import (
"encoding/json"
"net/http"
"strconv"

"github.com/HackIllinois/api/common/authtoken"
"github.com/HackIllinois/api/common/errors"
Expand All @@ -12,10 +13,14 @@ import (
"github.com/HackIllinois/api/services/profile/models"
"github.com/HackIllinois/api/services/profile/service"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"github.com/prometheus/client_golang/prometheus/promhttp"
)

var wsUpgrader = websocket.Upgrader{}

func SetupController(route *mux.Route) {
go service.LiveLeaderboardManager()
router := route.Subrouter()

router.Handle("/internal/metrics/", promhttp.Handler()).Methods("GET")
Expand All @@ -27,6 +32,7 @@ func SetupController(route *mux.Route) {

metrics.RegisterHandler("/list/", GetFilteredProfiles, "GET", router)
metrics.RegisterHandler("/leaderboard/", GetProfileLeaderboard, "GET", router)
metrics.RegisterHandler("/live/leaderboard/", GetLiveProfileLeaderboard, "GET", router)
metrics.RegisterHandler("/search/", GetValidFilteredProfiles, "GET", router)

metrics.RegisterHandler("/event/checkin/", RedeemEvent, "POST", router)
Expand Down Expand Up @@ -215,6 +221,28 @@ func GetProfileLeaderboard(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(user_profile_list)
}

func GetLiveProfileLeaderboard(w http.ResponseWriter, r *http.Request) {
parameters := r.URL.Query()

limit_param, ok := parameters["limit"]

if !ok {
limit_param = []string{"0"}
}

limit, err := strconv.Atoi(limit_param[0])
if err != nil {
errors.WriteError(w, r, errors.MalformedRequestError(err.Error(), "Limit was not correctly set"))
}

conn, err := wsUpgrader.Upgrade(w, r, nil)
if err != nil {
errors.WriteError(w, r, errors.InternalError(err.Error(), "Failed to upgrade to websocket connection"))
}

service.HandleIncomingLeaderboardWS(conn, limit)
}

/*
Filters the profiles by TeamStatus and Interests
*/
Expand Down Expand Up @@ -311,6 +339,8 @@ func AwardPoints(w http.ResponseWriter, r *http.Request) {
return
}

service.ChanUpdateLiveLeaderboard <- struct{}{}

updated_profile, err := service.GetProfile(profile_id)
if err != nil {
errors.WriteError(
Expand Down
Loading