From 7b8457d5091ed2987d9abc0998567104f6e20413 Mon Sep 17 00:00:00 2001 From: Namekuji Date: Sun, 4 Dec 2022 20:11:44 -0500 Subject: [PATCH] add room closing --- go.mod | 1 + go.sum | 1 + oauth.go | 6 ++- room.go | 151 ++++++++++++++++++++++++++++++++++++++++++++-------- schema.go | 28 ++++++++++ server.go | 17 +++--- webhooks.go | 35 ++++++++++++ 7 files changed, 209 insertions(+), 30 deletions(-) create mode 100644 webhooks.go diff --git a/go.mod b/go.mod index 476929a..7026e6f 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( ) require ( + github.com/go-logr/logr v1.2.3 // indirect github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect diff --git a/go.sum b/go.sum index 82daf64..0ec3241 100644 --- a/go.sum +++ b/go.sum @@ -15,6 +15,7 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/frostbyte73/go-throttle v0.0.0-20210621200530-8018c891361d h1:rvSueMilKro0jF+VfxoVR42wazKPl+cUBL3rFbiBGso= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= diff --git a/oauth.go b/oauth.go index b4c4b17..f3dfa4b 100644 --- a/oauth.go +++ b/oauth.go @@ -83,7 +83,7 @@ func oauthHandler(c echo.Context) (err error) { if errMsg := c.QueryParam("error"); errMsg == "access_denied" { return c.Redirect(http.StatusFound, "/login") } - return echo.NewHTTPError(http.StatusBadRequest, "authentication code needed") + return echo.NewHTTPError(http.StatusBadRequest, "auth_code_required") } data, err := getSessionData(c) @@ -153,7 +153,9 @@ func authMiddleware(next echo.HandlerFunc) echo.HandlerFunc { } if data.AudonID != "" { - if _, err := findUserByID(c.Request().Context(), data.AudonID); err == nil { + if user, err := findUserByID(c.Request().Context(), data.AudonID); err == nil { + c.Set("user", user) + c.Set("session", data) return next(c) } } diff --git a/room.go b/room.go index 0ea7485..0b5a26b 100644 --- a/room.go +++ b/room.go @@ -1,22 +1,25 @@ package main import ( + "context" + "errors" "net/http" "time" "github.com/jaevor/go-nanoid" "github.com/labstack/echo/v4" "github.com/livekit/protocol/auth" + "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" ) // handler for POST to /api/room -func createRoomHandler(c echo.Context) (err error) { +func createRoomHandler(c echo.Context) error { room := new(Room) - if err = c.Bind(room); err != nil { + if err := c.Bind(room); err != nil { return ErrInvalidRequestFormat } - if err = mainValidator.StructExcept(room, "RoomID"); err != nil { // New RoomID will be created, so one in request doesn't matter + if err := mainValidator.StructExcept(room, "RoomID"); err != nil { // New RoomID will be created, so one in request doesn't matter return wrapValidationError(err) } @@ -26,27 +29,15 @@ func createRoomHandler(c echo.Context) (err error) { } room.RoomID = canonic() - sessData, err := getSessionData(c) - if err != nil { - return err - } - - var host *AudonUser - host, err = findUserByID(c.Request().Context(), sessData.AudonID) - if err == mongo.ErrNoDocuments { - return c.JSON(http.StatusNotFound, []string{sessData.AudonID}) - } else if err != nil { - c.Logger().Error(err) - return echo.NewHTTPError(http.StatusInternalServerError) - } + host := c.Get("user").(*AudonUser) room.Host = host now := time.Now().UTC() - if now.Sub(room.ScheduledAt) > 0 { + if now.After(room.ScheduledAt) { room.ScheduledAt = now } - // If CoHosts are already registered, retrieve their data from DB + // if cohosts are already registered, retrieve their data from DB for i, cohost := range room.CoHost { cohostUser, err := findUserByRemote(c.Request().Context(), cohost.RemoteID, cohost.RemoteURL) if err == nil { @@ -64,13 +55,129 @@ func createRoomHandler(c echo.Context) (err error) { return c.String(http.StatusCreated, room.RoomID) } -func getHostToken(room *Room) (string, error) { +func joinRoomHandler(c echo.Context) error { + roomID := c.Param("id") + if err := mainValidator.Var(&roomID, "required,printascii"); err != nil { + return wrapValidationError(err) + } + + user := c.Get("user").(*AudonUser) + + room, err := findRoomByID(c.Request().Context(), roomID) + if err != nil { + return echo.NewHTTPError(http.StatusNotFound) + } + + now := time.Now().UTC() + + // check if room is not yet started + if room.ScheduledAt.After(now) { + return echo.NewHTTPError(http.StatusConflict, "not_yet_started") + } + + // check if room has already ended + if !room.EndedAt.IsZero() && room.EndedAt.Before(now) { + return echo.NewHTTPError(http.StatusGone, "already_ended") + } + + // when host or cohost joins + if room.IsHost(user) || room.IsCoHost(user) { + token, err := getRoomToken(room, user.AudonID, true) // host and cohost can talk from the beginning + if err != nil { + c.Logger().Error(err) + return echo.NewHTTPError(http.StatusInternalServerError) + } + + return c.JSON(http.StatusOK, token) + } + + // return 403 if one has been kicked + for _, kicked := range room.Kicked { + if kicked.Equal(user) { + return echo.NewHTTPError(http.StatusForbidden) + } + } + + // when one is neither host nor cohost + token, err := getRoomToken(room, user.AudonID, false) // listener needs a permission to talk + if err != nil { + c.Logger().Error(err) + return echo.NewHTTPError(http.StatusInternalServerError) + } + + return c.JSON(http.StatusOK, token) +} + +// intended to be called by room's host +func closeRoomHandler(c echo.Context) error { + roomID := c.Param("id") + if err := mainValidator.Var(&roomID, "required,printascii"); err != nil { + return wrapValidationError(err) + } + + // retrieve room info from the given room ID + room, err := findRoomByID(c.Request().Context(), roomID) + if err == mongo.ErrNoDocuments { + return c.String(http.StatusNotFound, "room_not_found") + } else if err != nil { + c.Logger().Error(err) + return echo.NewHTTPError(http.StatusInternalServerError) + } + + // only host can close the room + user := c.Get("user").(*AudonUser) + if !room.IsHost(user) { + return c.String(http.StatusForbidden, "must_be_host") + } + + if err := endRoom(c.Request().Context(), room); err != nil { + c.Logger().Error(err) + return echo.NewHTTPError(http.StatusInternalServerError) + } + + return c.NoContent(http.StatusOK) +} + +func getRoomToken(room *Room, identity string, canTalk bool) (string, error) { at := auth.NewAccessToken(mainConfig.Livekit.APIKey, mainConfig.Livekit.APISecret) grant := &auth.VideoGrant{ - Room: room.RoomID, - RoomJoin: true, + Room: room.RoomID, + RoomJoin: true, + CanPublish: &canTalk, } - at.AddGrant(grant).SetIdentity(room.Host.AudonID).SetValidFor(10 * time.Minute) + at.AddGrant(grant).SetIdentity(identity).SetValidFor(10 * time.Minute) return at.ToJWT() } + +func findRoomByID(ctx context.Context, roomID string) (*Room, error) { + var room Room + collRoom := mainDB.Collection(COLLECTION_ROOM) + if err := collRoom.FindOne(ctx, bson.D{{Key: "room_id", Value: roomID}}).Decode(&room); err != nil { + return nil, err + } + return &room, nil +} + +func endRoom(ctx context.Context, room *Room) error { + if room == nil { + return errors.New("room cannot be nil") + } + + if !room.EndedAt.IsZero() { + return nil + } + + now := time.Now().UTC() + + collRoom := mainDB.Collection(COLLECTION_ROOM) + if _, err := collRoom.UpdateOne(ctx, + bson.D{{Key: "room_id", Value: room.RoomID}}, + bson.D{ + {Key: "$set", Value: bson.D{{Key: "ended_at", Value: now}}}, + }); err != nil { + return err + } + + return nil +} diff --git a/schema.go b/schema.go index 0ca37fd..43b1878 100644 --- a/schema.go +++ b/schema.go @@ -33,7 +33,9 @@ type ( FollowingOnly bool `bson:"following_only" json:"following_only"` FollowerOnly bool `bson:"follower_only" json:"follower_only"` MutualOnly bool `bson:"mutual_only" json:"mutual_only"` + Kicked []*AudonUser `bson:"kicked" json:"kicked"` ScheduledAt time.Time `bson:"scheduled_at" json:"scheduled_at"` + EndedAt time.Time `bson:"ended_at" json:"ended_at"` CreatedAt time.Time `bson:"created_at" json:"created_at"` } ) @@ -43,6 +45,32 @@ const ( COLLECTION_ROOM = "room" ) +func (a *AudonUser) Equal(u *AudonUser) bool { + if a == nil { + return false + } + + return a.AudonID == u.AudonID || (a.RemoteID == u.RemoteID && a.RemoteURL == u.RemoteURL) +} + +func (r *Room) IsCoHost(u *AudonUser) bool { + if r == nil { + return false + } + + for _, cohost := range r.CoHost { + if cohost.Equal(u) { + return true + } + } + + return false +} + +func (r *Room) IsHost(u *AudonUser) bool { + return r != nil && r.Host.Equal(u) +} + func createIndexes(ctx context.Context) error { userColl := mainDB.Collection(COLLECTION_USER) userIndexes, err := userColl.Indexes().ListSpecifications(ctx) diff --git a/server.go b/server.go index 59f3fa3..266d083 100644 --- a/server.go +++ b/server.go @@ -3,7 +3,6 @@ package main import ( "context" "encoding/gob" - "errors" "html/template" "io" "log" @@ -34,11 +33,10 @@ type ( ) var ( - err_invalid_cookie error = errors.New("invalid cookie") - mastAppConfigBase *mastodon.AppConfig = nil - mainDB *mongo.Database = nil - mainValidator = validator.New() - mainConfig *AppConfig + mastAppConfigBase *mastodon.AppConfig = nil + mainDB *mongo.Database = nil + mainValidator = validator.New() + mainConfig *AppConfig ) func init() { @@ -72,6 +70,7 @@ func main() { os.Exit(3) } + // Setup echo server e := echo.New() defer e.Close() @@ -80,6 +79,8 @@ func main() { } e.Renderer = t e.Validator = &CustomValidator{validator: mainValidator} + + // Setup session middleware (currently Audon stores all client data in cookie) cookieStore := sessions.NewCookieStore([]byte(mainConfig.SeesionSecret)) cookieStore.Options = &sessions.Options{ Path: "/", @@ -106,6 +107,10 @@ func main() { api := e.Group("/api", authMiddleware) api.POST("/room", createRoomHandler) + api.GET("/room/:id", joinRoomHandler) + api.DELETE("/room/:id", closeRoomHandler) + + e.POST("/app/webhook", livekitWebhookHandler) e.Logger.Debug(e.Start(":1323")) } diff --git a/webhooks.go b/webhooks.go new file mode 100644 index 0000000..416b7cb --- /dev/null +++ b/webhooks.go @@ -0,0 +1,35 @@ +package main + +import ( + "net/http" + + "github.com/labstack/echo/v4" + "github.com/livekit/protocol/auth" + "github.com/livekit/protocol/webhook" +) + +func livekitWebhookHandler(c echo.Context) error { + authProvider := auth.NewSimpleKeyProvider(mainConfig.Livekit.APIKey, mainConfig.Livekit.APISecret) + event, err := webhook.ReceiveWebhookEvent(c.Request(), authProvider) + + if err == webhook.ErrNoAuthHeader { + return echo.NewHTTPError(http.StatusForbidden) + } + + if event.GetEvent() == webhook.EventRoomFinished { + roomID := event.GetRoom().GetName() + if err := mainValidator.Var(&roomID, "required,printascii"); err == nil { + room, err := findRoomByID(c.Request().Context(), roomID) + if err == nil { + if err := endRoom(c.Request().Context(), room); err != nil { + c.Logger().Error(err) + return echo.NewHTTPError(http.StatusInternalServerError) + } + } + } + + return c.NoContent(http.StatusOK) + } + + return echo.NewHTTPError(http.StatusNotFound) +}