diff --git a/pkg/internal/models/calls.go b/pkg/internal/models/calls.go index 2e378f5..e20114b 100644 --- a/pkg/internal/models/calls.go +++ b/pkg/internal/models/calls.go @@ -1,6 +1,9 @@ package models -import "time" +import ( + "github.com/livekit/protocol/livekit" + "time" +) type Call struct { BaseModel @@ -12,4 +15,6 @@ type Call struct { ChannelID uint `json:"channel_id"` Founder ChannelMember `json:"founder"` Channel Channel `json:"channel"` + + Participants []*livekit.ParticipantInfo `json:"participants" gorm:"-"` } diff --git a/pkg/internal/server/api/calls_api.go b/pkg/internal/server/api/calls_api.go index a5e508f..f96e057 100644 --- a/pkg/internal/server/api/calls_api.go +++ b/pkg/internal/server/api/calls_api.go @@ -4,12 +4,16 @@ import ( "git.solsynth.dev/hydrogen/messaging/pkg/internal/database" "git.solsynth.dev/hydrogen/messaging/pkg/internal/gap" "git.solsynth.dev/hydrogen/messaging/pkg/internal/models" + "git.solsynth.dev/hydrogen/messaging/pkg/internal/server/exts" "git.solsynth.dev/hydrogen/messaging/pkg/internal/services" "github.com/gofiber/fiber/v2" "github.com/google/uuid" "github.com/spf13/viper" + "sync" ) +var callLocks sync.Map + func listCall(c *fiber.Ctx) error { take := c.QueryInt("take", 0) offset := c.QueryInt("offset", 0) @@ -41,13 +45,16 @@ func getOngoingCall(c *fiber.Ctx) error { if call, err := services.GetOngoingCall(channel); err != nil { return fiber.NewError(fiber.StatusNotFound, err.Error()) + } else if res, err := services.GetCallParticipants(call); err != nil { + return c.JSON(call) } else { + call.Participants = res return c.JSON(call) } } func startCall(c *fiber.Ctx) error { - if err := gap.H.EnsureAuthenticated(c); err != nil { + if err := gap.H.EnsureGrantedPerm(c, "CreateCalls", true); err != nil { return err } user := c.Locals("user").(models.Account) @@ -66,10 +73,19 @@ func startCall(c *fiber.Ctx) error { AccountID: user.ID, }).Find(&membership).Error; err != nil { return fiber.NewError(fiber.StatusNotFound, err.Error()) + } else if membership.PowerLevel < 0 { + return fiber.NewError(fiber.StatusForbidden, "you have not enough permission to create a call") + } + + if _, ok := callLocks.Load(channel.ID); ok { + return fiber.NewError(fiber.StatusLocked, "there is already a call in creation progress for this channel") + } else { + callLocks.Store(channel.ID, true) } call, err := services.NewCall(channel, membership) if err != nil { + callLocks.Delete(channel.ID) return fiber.NewError(fiber.StatusBadRequest, err.Error()) } else { _, _ = services.NewEvent(models.Event{ @@ -82,6 +98,7 @@ func startCall(c *fiber.Ctx) error { SenderID: membership.ID, }) + callLocks.Delete(channel.ID) return c.JSON(call) } } @@ -111,7 +128,7 @@ func endCall(c *fiber.Ctx) error { call, err := services.GetOngoingCall(channel) if err != nil { return fiber.NewError(fiber.StatusNotFound, err.Error()) - } else if call.FounderID != user.ID && membership.PowerLevel < 100 { + } else if call.FounderID != user.ID && membership.PowerLevel < 50 { return fiber.NewError(fiber.StatusBadRequest, "only call founder or channel admin can end this call") } @@ -132,6 +149,48 @@ func endCall(c *fiber.Ctx) error { } } +func kickParticipantInCall(c *fiber.Ctx) error { + if err := gap.H.EnsureAuthenticated(c); err != nil { + return err + } + user := c.Locals("user").(models.Account) + alias := c.Params("channel") + + var data struct { + Username string `json:"username" validate:"required"` + } + if err := exts.BindAndValidate(c, &data); err != nil { + return err + } + + var channel models.Channel + if err := database.C.Where(&models.Channel{ + Alias: alias, + }).First(&channel).Error; err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } + + var membership models.ChannelMember + if err := database.C.Where(&models.ChannelMember{ + ChannelID: channel.ID, + AccountID: user.ID, + }).Find(&membership).Error; err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } + + call, err := services.GetOngoingCall(channel) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } else if call.FounderID != user.ID && membership.PowerLevel < 50 { + return fiber.NewError(fiber.StatusBadRequest, "only call founder or channel admin can kick participant in this call") + } + + if err = services.KickParticipantInCall(call, data.Username); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + return c.SendStatus(fiber.StatusOK) +} + func exchangeCallToken(c *fiber.Ctx) error { if err := gap.H.EnsureAuthenticated(c); err != nil { return err diff --git a/pkg/internal/server/api/channels_api.go b/pkg/internal/server/api/channels_api.go index fe8dde5..dcddea1 100644 --- a/pkg/internal/server/api/channels_api.go +++ b/pkg/internal/server/api/channels_api.go @@ -105,7 +105,7 @@ func listAvailableChannel(c *fiber.Ctx) error { } func createChannel(c *fiber.Ctx) error { - if err := gap.H.EnsureAuthenticated(c); err != nil { + if err := gap.H.EnsureGrantedPerm(c, "CreateChannels", true); err != nil { return err } user := c.Locals("user").(models.Account) diff --git a/pkg/internal/server/api/index.go b/pkg/internal/server/api/index.go index 7704164..7b69c03 100644 --- a/pkg/internal/server/api/index.go +++ b/pkg/internal/server/api/index.go @@ -43,6 +43,7 @@ func MapAPIs(app *fiber.App, baseURL string) { channels.Get("/:channel/calls/ongoing", getOngoingCall) channels.Post("/:channel/calls", startCall) channels.Delete("/:channel/calls/ongoing", endCall) + channels.Delete("/:channel/calls/ongoing/participant", kickParticipantInCall) channels.Post("/:channel/calls/ongoing/token", exchangeCallToken) } } diff --git a/pkg/internal/services/calls.go b/pkg/internal/services/calls.go index 8d605ac..61c9700 100644 --- a/pkg/internal/services/calls.go +++ b/pkg/internal/services/calls.go @@ -66,6 +66,16 @@ func GetOngoingCall(channel models.Channel) (models.Call, error) { } } +func GetCallParticipants(call models.Call) ([]*livekit.ParticipantInfo, error) { + res, err := Lk.ListParticipants(context.Background(), &livekit.ListParticipantsRequest{ + Room: call.ExternalID, + }) + if err != nil { + return nil, err + } + return res.Participants, nil +} + func NewCall(channel models.Channel, founder models.ChannelMember) (models.Call, error) { call := models.Call{ ExternalID: channel.Alias, @@ -94,7 +104,7 @@ func NewCall(channel models.Channel, founder models.ChannelMember) (models.Call, } else if err = database.C.Where(models.ChannelMember{ ChannelID: call.ChannelID, }).Preload("Account").Find(&members).Error; err == nil { - channel := call.Channel + channel = call.Channel call, _ = GetCall(call.Channel, call.ID) for _, member := range members { if member.ID != call.Founder.ID { @@ -146,6 +156,14 @@ func EndCall(call models.Call) (models.Call, error) { return call, nil } +func KickParticipantInCall(call models.Call, username string) error { + _, err := Lk.RemoveParticipant(context.Background(), &livekit.RoomParticipantIdentity{ + Room: call.ExternalID, + Identity: username, + }) + return err +} + func EncodeCallToken(user models.Account, call models.Call) (string, error) { isAdmin := false if user.ID == call.FounderID || user.ID == call.Channel.AccountID {