diff --git a/.devcontainer/livekit.yaml b/.devcontainer/livekit.yaml index ee4e875..18f35d2 100644 --- a/.devcontainer/livekit.yaml +++ b/.devcontainer/livekit.yaml @@ -20,7 +20,7 @@ webhook: room: auto_create: false - empty_timeout: 30 + empty_timeout: 3600 max_participants: 0 max_metadata_size: 0 diff --git a/audon-fe/src/components/JoinDialog.vue b/audon-fe/src/components/JoinDialog.vue new file mode 100644 index 0000000..1b00ec0 --- /dev/null +++ b/audon-fe/src/components/JoinDialog.vue @@ -0,0 +1,173 @@ + + + + + + + + + + {{ $t("onlineIndicator.message") }} + + + + + + + + {{ $t("onlineIndicator.hint") }} + + + {{ $t("onlineIndicator.warning") }} + + + {{ $t("onlineIndicator.nope") }} + {{ + $t("onlineIndicator.sure") + }} + + + + + {{ $t("browserMuted") }} + + + {{ + $t("startListening") + }} + + + + diff --git a/audon-fe/src/components/Participant.vue b/audon-fe/src/components/Participant.vue index 4f169e7..c74fe94 100644 --- a/audon-fe/src/components/Participant.vue +++ b/audon-fe/src/components/Participant.vue @@ -1,3 +1,4 @@ + + + + + + + + + + + {{ $t("editRoom") }} @@ -760,23 +723,12 @@ export default { - - - - {{ $t("browserMuted") }} - - - {{ - $t("startListening") - }} - - - {{ - $t("leaveRoom") - }} - - - + {{ $t("speakRequest.label") }} diff --git a/auth.go b/auth.go index 91a9535..5983c85 100644 --- a/auth.go +++ b/auth.go @@ -177,7 +177,7 @@ func oauthHandler(c echo.Context) (err error) { // return c.Redirect(http.StatusFound, "http://localhost:5173") } -func getOAuthTokenHandler(c echo.Context) (err error) { +func getUserTokenHandler(c echo.Context) (err error) { data, ok := c.Get("data").(*SessionData) if !ok { return ErrInvalidSession diff --git a/avatar.go b/avatar.go index f97c725..0f29f79 100644 --- a/avatar.go +++ b/avatar.go @@ -47,9 +47,6 @@ func (u *AudonUser) GetIndicator(ctx context.Context, fnew []byte) ([]byte, erro if err := os.WriteFile(saved, fnew, 0664); err != nil { return nil, err } - if u.AvatarFile != "" { - // os.Remove(u.getAvatarImagePath(u.AvatarFile)) - } isAvatarNew = true } @@ -94,7 +91,7 @@ func (u *AudonUser) createGIF(avatar image.Image) ([]byte, error) { baseFrame := image.NewRGBA(avatarPNG.Bounds()) draw.Draw(baseFrame, baseFrame.Bounds(), image.Black, image.Point{}, draw.Src) draw.Copy(baseFrame, image.Point{}, avatarPNG, avatarPNG.Bounds(), draw.Over, nil) - draw.Draw(baseFrame, baseFrame.Bounds(), mainConfig.LogoImageBack, image.Point{-35, -35}, draw.Over) + draw.Draw(baseFrame, baseFrame.Bounds(), mainConfig.LogoImageBack, image.Point{-55, -105}, draw.Over) anim := webpanimation.NewWebpAnimation(150, 150, 0) defer anim.ReleaseMemory() @@ -115,7 +112,7 @@ func (u *AudonUser) createGIF(avatar image.Image) ([]byte, error) { } mask := image.NewUniform(color.Alpha{alpha}) - draw.DrawMask(frame, frame.Bounds(), mainConfig.LogoImageFront, image.Point{-35, -35}, mask, image.Point{}, draw.Over) + draw.DrawMask(frame, frame.Bounds(), mainConfig.LogoImageFront, image.Point{-55, -105}, mask, image.Point{}, draw.Over) if err := anim.AddFrame(frame, 1000/count*i, webpConf); err != nil { return nil, err diff --git a/public/logo_back.png b/public/logo_back.png index 2a2eafd..f2d22e1 100644 Binary files a/public/logo_back.png and b/public/logo_back.png differ diff --git a/public/logo_front.png b/public/logo_front.png index cf1bff5..9f0ded4 100644 Binary files a/public/logo_front.png and b/public/logo_front.png differ diff --git a/room.go b/room.go index ac54706..207f376 100644 --- a/room.go +++ b/room.go @@ -299,7 +299,7 @@ func joinRoomHandler(c echo.Context) (err error) { } } - roomMetadata := &RoomMetadata{Room: room} + roomMetadata := &RoomMetadata{Room: room, MastodonAccounts: make(map[string]*MastodonAccount)} // Allows the user to talk if the user is a speaker lkRoom, _ := getRoomInLivekit(c.Request().Context(), room.RoomID) // lkRoom will be nil if it doesn't exist @@ -326,21 +326,43 @@ func joinRoomHandler(c echo.Context) (err error) { Token: token, Audon: user, } - if user.AvatarFile != "" { - orig, err := os.ReadFile(user.getAvatarImagePath(user.AvatarFile)) - if err == nil && orig != nil { - resp.Original = fmt.Sprintf("data:%s;base64,%s", mimetype.Detect(orig), base64.StdEncoding.EncodeToString(orig)) - } + + mastoAccount := new(MastodonAccount) + if err := c.Bind(&mastoAccount); err != nil { + c.Logger().Error(err) + return ErrInvalidRequestFormat } - avatarLink := c.FormValue("avatar") - if avatarLink != "" { + roomMetadata.MastodonAccounts[user.AudonID] = mastoAccount + + // Get ready to change avatar if user is host or cohost + if room.IsHost(user) || room.IsCoHost(user) { + // Get user's stored avatar if exists + if user.AvatarFile != "" { + orig, err := os.ReadFile(user.getAvatarImagePath(user.AvatarFile)) + if err == nil && orig != nil { + resp.Original = fmt.Sprintf("data:%s;base64,%s", mimetype.Detect(orig), base64.StdEncoding.EncodeToString(orig)) + } else if orig == nil { + user.AvatarFile = "" + } + // icon, err := os.ReadFile(user.GetGIFAvatarPath()) + // if err == nil && icon != nil { + // resp.Indicator = fmt.Sprintf("data:image/gif;base64,%s", base64.StdEncoding.EncodeToString(icon)) + // } + } + avatarLink := mastoAccount.Avatar + if err := mainValidator.Var(&avatarLink, "required"); err != nil { + return wrapValidationError(err) + } avatarURL, err := url.Parse(avatarLink) if err != nil { + c.Logger().Error(err) return ErrInvalidRequestFormat } - if online, err := user.InLivekit(c.Request().Context()); !online && err == nil { + // Retrieve user's current avatar if the old one doesn't exist in Audon. + // Skips if user is still in another room. + if already, err := user.InLivekit(c.Request().Context()); !already && err == nil && user.AvatarFile == "" { // Download user's avatar req, err := http.NewRequest(http.MethodGet, avatarURL.String(), nil) if err != nil { @@ -362,12 +384,14 @@ func joinRoomHandler(c echo.Context) (err error) { return echo.NewHTTPError(http.StatusInternalServerError) } + // Generate indicator GIF indicator, err := user.GetIndicator(c.Request().Context(), fnew) if err != nil { - c.Logger().Warn(err) + c.Logger().Error(err) + return echo.NewHTTPError(http.StatusInternalServerError) } - resp.Indicator = fmt.Sprintf("data:image/gif;base64,%s", base64.StdEncoding.EncodeToString(indicator)) resp.Original = fmt.Sprintf("data:%s;base64,%s", mimetype.Detect(fnew), base64.StdEncoding.EncodeToString(fnew)) + resp.Indicator = fmt.Sprintf("data:image/gif;base64,%s", base64.StdEncoding.EncodeToString(indicator)) } else if err != nil { c.Logger().Error(err) } @@ -392,6 +416,26 @@ func joinRoomHandler(c echo.Context) (err error) { c.Logger().Error(err) return echo.NewHTTPError(http.StatusConflict) } + } else { + currentMeta, err := getRoomMetadataFromLivekitRoom(lkRoom) + if err != nil { + c.Logger().Error(err) + return echo.NewHTTPError(http.StatusInternalServerError) + } + currentMeta.MastodonAccounts[user.AudonID] = mastoAccount + newMetadata, err := json.Marshal(currentMeta) + if err != nil { + c.Logger().Error(err) + return echo.NewHTTPError(http.StatusInternalServerError) + } + _, err = lkRoomServiceClient.UpdateRoomMetadata(c.Request().Context(), &livekit.UpdateRoomMetadataRequest{ + Room: roomID, + Metadata: string(newMetadata), + }) + if err != nil { + c.Logger().Error(err) + return echo.NewHTTPError(http.StatusInternalServerError) + } } // Store user's session data in cache @@ -444,7 +488,7 @@ func leaveRoomHandler(c echo.Context) error { c.Logger().Error(err) return echo.NewHTTPError(http.StatusInternalServerError) } else if still { - return c.NoContent(http.StatusAccepted) + return c.NoContent(http.StatusConflict) } if err := user.ClearUserAvatar(c.Request().Context()); err != nil { c.Logger().Error(err) diff --git a/schema.go b/schema.go index 453d820..ccfea10 100644 --- a/schema.go +++ b/schema.go @@ -30,7 +30,8 @@ type ( RoomMetadata struct { *Room - Speakers []*AudonUser `json:"speakers"` + Speakers []*AudonUser `json:"speakers"` + MastodonAccounts map[string]*MastodonAccount `json:"accounts"` } Room struct { diff --git a/server.go b/server.go index 96a5e0e..a434fa5 100644 --- a/server.go +++ b/server.go @@ -169,7 +169,8 @@ func main() { e.POST("/app/webhook", livekitWebhookHandler) api := e.Group("/api", authMiddleware) - api.GET("/token", getOAuthTokenHandler) + api.GET("/token", getUserTokenHandler) + // api.GET("/room", getStatusHandler) api.POST("/room", createRoomHandler) api.DELETE("/room", leaveRoomHandler) api.POST("/room/:id", joinRoomHandler) diff --git a/user.go b/user.go index 73d1e66..e89e432 100644 --- a/user.go +++ b/user.go @@ -3,11 +3,37 @@ package main import ( "context" "net/http" + "time" "github.com/labstack/echo/v4" + mastodon "github.com/mattn/go-mastodon" "go.mongodb.org/mongo-driver/bson" ) +type MastodonAccount struct { + ID mastodon.ID `json:"id"` + Username string `json:"username"` + Acct string `json:"acct"` + DisplayName string `json:"displayName"` + Locked bool `json:"locked"` + CreatedAt time.Time `json:"createdAt"` + FollowersCount int64 `json:"followersCount"` + FollowingCount int64 `json:"followingCount"` + StatusesCount int64 `json:"statusesCount"` + Note string `json:"note"` + URL string `json:"url"` + Avatar string `json:"avatar"` + AvatarStatic string `json:"avatarStatic"` + Header string `json:"header"` + HeaderStatic string `json:"headerStatic"` + Emojis []mastodon.Emoji `json:"emojis"` + Moved *MastodonAccount `json:"moved"` + Fields []mastodon.Field `json:"fields"` + Bot bool `json:"bot"` + Discoverable bool `json:"discoverable"` + Source *mastodon.AccountSource `json:"source"` +} + func getUserHandler(c echo.Context) error { audonID := c.Param("id") if err := mainValidator.Var(&audonID, "required,printascii"); err != nil { @@ -22,6 +48,18 @@ func getUserHandler(c echo.Context) error { return c.JSON(http.StatusOK, user) } +func getStatusHandler(c echo.Context) error { + u := c.Get("user").(*AudonUser) + + ids, err := u.GetCurrentRoomIDs(c.Request().Context()) + if err != nil { + c.Logger().Error(err) + return echo.NewHTTPError(http.StatusInternalServerError) + } + + return c.JSON(http.StatusOK, ids) +} + func (a *AudonUser) Equal(u *AudonUser) bool { if a == nil { return false @@ -40,12 +78,26 @@ func (a *AudonUser) InLivekit(ctx context.Context) (bool, error) { } func (a *AudonUser) ClearUserAvatar(ctx context.Context) error { - // os.Remove(a.getAvatarImagePath(a.AvatarFile)) coll := mainDB.Collection(COLLECTION_USER) _, err := coll.UpdateOne(ctx, bson.D{{Key: "audon_id", Value: a.AudonID}}, bson.D{ {Key: "$set", Value: bson.D{{Key: "avatar", Value: ""}}}, }) + // if err == nil { + // os.Remove(a.getAvatarImagePath(a.AvatarFile)) + // } return err } + +func (a *AudonUser) GetCurrentRoomIDs(ctx context.Context) ([]string, error) { + rooms, err := a.GetCurrentLivekitRooms(ctx) + if err != nil { + return nil, err + } + roomIDs := make([]string, len(rooms)) + for i, r := range rooms { + roomIDs[i] = r.GetName() + } + return roomIDs, nil +} diff --git a/webhooks.go b/webhooks.go index 1316ec8..c7c05ba 100644 --- a/webhooks.go +++ b/webhooks.go @@ -25,7 +25,8 @@ func livekitWebhookHandler(c echo.Context) error { } if event.GetEvent() == webhook.EventRoomFinished { - room, err := findRoomByID(c.Request().Context(), event.GetRoom().GetName()) + lkRoom := event.GetRoom() + room, err := findRoomByID(c.Request().Context(), lkRoom.GetName()) if err != nil { c.Logger().Error(err) return echo.NewHTTPError(http.StatusNotFound) @@ -66,34 +67,36 @@ func livekitWebhookHandler(c echo.Context) error { countdown := time.NewTimer(10 * time.Second) webhookTimerCache.Set(audonID, countdown, ttlcache.DefaultTTL) - <-countdown.C - webhookTimerCache.Delete(audonID) - ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) - defer cancel() - // ctx := context.TODO() + go func() { + <-countdown.C + webhookTimerCache.Delete(audonID) + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() - stillAgain, err := user.InLivekit(ctx) - if stillAgain || err != nil { - return c.NoContent(http.StatusOK) - } - user, err = findUserByID(ctx, audonID) - if err == nil && user.AvatarFile != "" { - log.Printf("restoring avatar: %s\n", audonID) + stillAgain, err := user.InLivekit(ctx) if err != nil { - c.Logger().Error(err) - return echo.NewHTTPError(http.StatusInternalServerError) + log.Println(err) } - avatar := user.getAvatarImagePath(user.AvatarFile) - _, err = updateAvatar(ctx, mastoClient, avatar) - if err != nil { - c.Logger().Warn(err) + if stillAgain { + return } - user.ClearUserAvatar(ctx) - // os.Remove(avatar) - } else if err != nil { - c.Logger().Error(err) - return echo.NewHTTPError(http.StatusInternalServerError) - } + nextUser, err := findUserByID(ctx, audonID) + if err == nil && nextUser.AvatarFile != "" { + log.Printf("restoring avatar: %s\n", audonID) + if err != nil { + log.Println(err) + return + } + avatar := nextUser.getAvatarImagePath(nextUser.AvatarFile) + _, err = updateAvatar(ctx, mastoClient, avatar) + if err != nil { + log.Println(err) + } + nextUser.ClearUserAvatar(ctx) + } else if err != nil { + log.Println(err) + } + }() } return c.NoContent(http.StatusOK) } else if event.GetEvent() == webhook.EventRoomStarted {