diff --git a/app/controllers/RelayRound.scala b/app/controllers/RelayRound.scala index 434cb64c1974f..d8ea77ae9b273 100644 --- a/app/controllers/RelayRound.scala +++ b/app/controllers/RelayRound.scala @@ -16,14 +16,14 @@ final class RelayRound( apiC: => Api ) extends LilaController(env): - def form(tourId: String) = Auth { ctx ?=> _ ?=> + def form(tourId: TourModel.Id) = Auth { ctx ?=> _ ?=> NoLameOrBot: WithTourAndRoundsCanUpdate(tourId): trs => Ok.page: html.relay.roundForm.create(env.relay.roundForm.create(trs), trs.tour) } - def create(tourId: String) = AuthOrScopedBody(_.Study.Write) { ctx ?=> me ?=> + def create(tourId: TourModel.Id) = AuthOrScopedBody(_.Study.Write) { ctx ?=> me ?=> NoLameOrBot: WithTourAndRoundsCanUpdate(tourId): trs => val tour = trs.tour diff --git a/app/controllers/Study.scala b/app/controllers/Study.scala index 4125f8f2e1415..db97a7b27dbf5 100644 --- a/app/controllers/Study.scala +++ b/app/controllers/Study.scala @@ -6,8 +6,8 @@ import scala.util.chaining.* import lila.app.{ given, * } import lila.common.paginator.{ Paginator, PaginatorJson } -import lila.common.{ HTTPRequest, IpAddress } -import lila.study.actorApi.Who +import lila.common.{ Bus, HTTPRequest, IpAddress } +import lila.study.actorApi.{ BecomeStudyAdmin, Who } import lila.study.JsonView.JsData import lila.study.Study.WithChapter import lila.study.{ Order, StudyForm, Study as StudyModel } @@ -314,8 +314,9 @@ final class Study( } def admin(id: StudyId) = Secure(_.StudyAdmin) { ctx ?=> me ?=> + Bus.publish(BecomeStudyAdmin(id, me), "adminStudy") env.study.api - .adminInvite(id) + .becomeAdmin(id, me) .inject: if HTTPRequest.isXhr(ctx.req) then NoContent else Redirect(routes.Study.show(id)) } diff --git a/modules/relay/src/main/Env.scala b/modules/relay/src/main/Env.scala index e1d8b96c22fc9..033e52b0bc1c9 100644 --- a/modules/relay/src/main/Env.scala +++ b/modules/relay/src/main/Env.scala @@ -75,6 +75,12 @@ final class Env( _ so api.requestPlay(id into RelayRoundId, v) } }, + "kickStudy" -> { case lila.study.actorApi.Kick(studyId, userId, who) => + roundRepo.tourIdByStudyId(studyId).flatMapz(api.kickBroadcast(userId, _, who)) + }, + "adminStudy" -> { case lila.study.actorApi.BecomeStudyAdmin(studyId, me) => + api.becomeStudyAdmin(studyId, me) + }, "isOfficialRelay" -> { case lila.study.actorApi.IsOfficialRelay(studyId, promise) => promise completeWith api.isOfficial(studyId) } diff --git a/modules/relay/src/main/RelayApi.scala b/modules/relay/src/main/RelayApi.scala index 6b90b6dbaf22c..b9596c8f141ca 100644 --- a/modules/relay/src/main/RelayApi.scala +++ b/modules/relay/src/main/RelayApi.scala @@ -12,7 +12,7 @@ import lila.db.dsl.{ *, given } import lila.memo.CacheApi import lila.study.{ Settings, Study, StudyApi, StudyId, StudyMaker, StudyMultiBoard, StudyRepo } import lila.security.Granter -import lila.user.{ User, Me } +import lila.user.{ User, Me, MyId } import lila.relay.RelayTour.ActiveWithSomeRounds final class RelayApi( @@ -58,6 +58,13 @@ final class RelayApi( def byTourOrdered(tour: RelayTour): Fu[List[RelayRound.WithTour]] = roundRepo.byTourOrdered(tour).dmap(_.map(_ withTour tour)) + def roundIdsById(tourId: RelayTour.Id): Fu[List[StudyId]] = + roundRepo.idsByTourId(tourId) + + def kickBroadcast(userId: UserId, tourId: RelayTour.Id, who: MyId): Funit = + roundIdsById(tourId).flatMap: + _.map(studyApi.kick(_, userId, who)).parallel.void + def withRounds(tour: RelayTour) = roundRepo.byTourOrdered(tour).dmap(tour.withRounds) def denormalizeTourActive(tourId: RelayTour.Id): Funit = @@ -373,6 +380,13 @@ final class RelayApi( private[relay] def onStudyRemove(studyId: StudyId) = roundRepo.coll.delete.one($id(studyId into RelayRoundId)).void + private[relay] def becomeStudyAdmin(studyId: StudyId, me: Me): Funit = + roundRepo + .tourIdByStudyId(studyId) + .flatMapz: tourId => + roundIdsById(tourId).flatMap: + _.map(studyApi.becomeAdmin(_, me)).sequence.void + private def sendToContributors(id: RelayRoundId, t: String, msg: JsObject): Funit = studyApi members id.into(StudyId) map { _.map(_.contributorIds).withFilter(_.nonEmpty) foreach { userIds => diff --git a/modules/relay/src/main/RelayRound.scala b/modules/relay/src/main/RelayRound.scala index 8093ab1548f0e..afce443979abd 100644 --- a/modules/relay/src/main/RelayRound.scala +++ b/modules/relay/src/main/RelayRound.scala @@ -6,6 +6,7 @@ import lila.study.Study import lila.common.Seconds case class RelayRound( + /* Same as the Study id it refers to */ _id: RelayRoundId, tourId: RelayTour.Id, name: RelayRoundName, diff --git a/modules/relay/src/main/RelayRoundRepo.scala b/modules/relay/src/main/RelayRoundRepo.scala index d01d297e6c4a3..ebb7a13bed2f1 100644 --- a/modules/relay/src/main/RelayRoundRepo.scala +++ b/modules/relay/src/main/RelayRoundRepo.scala @@ -17,12 +17,22 @@ final private class RelayRoundRepo(val coll: Coll)(using Executor): .list(RelayTour.maxRelays) def idsByTourOrdered(tour: RelayTour): Fu[List[RelayRoundId]] = + coll.primitive[RelayRoundId]( + selector = selectors.tour(tour.id), + sort = sort.chrono, + nb = RelayTour.maxRelays, + field = "_id" + ) + + def tourIdByStudyId(studyId: StudyId): Fu[Option[RelayTour.Id]] = + coll.primitiveOne[RelayTour.Id]($id(studyId), "tourId") + + def idsByTourId(tourId: RelayTour.Id): Fu[List[StudyId]] = coll - .find(selectors.tour(tour.id), $id(true).some) - .sort(sort.chrono) + .find(selectors.tour(tourId)) .cursor[Bdoc]() .list(RelayTour.maxRelays) - .map(_.flatMap(_.getAsOpt[RelayRoundId]("_id"))) + .map(_.flatMap(_.getAsOpt[StudyId]("_id"))) def lastByTour(tour: RelayTour): Fu[Option[RelayRound]] = coll diff --git a/modules/study/src/main/StudyApi.scala b/modules/study/src/main/StudyApi.scala index 3e6ea60d7cbe1..20cebf0af7d25 100644 --- a/modules/study/src/main/StudyApi.scala +++ b/modules/study/src/main/StudyApi.scala @@ -13,7 +13,7 @@ import lila.security.Granter import lila.socket.Socket.Sri import lila.tree.Node.{ Comment, Gamebook, Shapes } import lila.tree.{ Branch, Branches } -import lila.user.{ Me, User } +import lila.user.{ Me, User, MyId } final class StudyApi( studyRepo: StudyRepo, @@ -362,13 +362,13 @@ final class StudyApi( ) .void - def kick(studyId: StudyId, userId: UserId)(who: Who) = + def kick(studyId: StudyId, userId: UserId, who: MyId) = sequenceStudy(studyId): study => studyRepo - .isAdminMember(study, who.u) + .isAdminMember(study, who) .flatMap: isAdmin => val allowed = study.isMember(userId) && { - (isAdmin && !study.isOwner(userId)) || (study.isOwner(who.u) ^ (who.u == userId)) + (isAdmin && !study.isOwner(userId)) || (study.isOwner(who) ^ (who is userId)) } allowed.so: studyRepo.removeMember(study, userId) andDo @@ -777,8 +777,8 @@ final class StudyApi( Contribute(by.id, study): chapterRepo deleteByStudy study - def adminInvite(studyId: StudyId)(using Me): Funit = - sequenceStudy(studyId)(inviter.admin) + def becomeAdmin(studyId: StudyId, me: MyId): Funit = + sequenceStudy(studyId)(inviter.becomeAdmin(me)) private def indexStudy(study: Study) = Bus.publish(actorApi.SaveStudy(study), "study") diff --git a/modules/study/src/main/StudyInvite.scala b/modules/study/src/main/StudyInvite.scala index 81a12b9cd8cd8..ecaf58afa202e 100644 --- a/modules/study/src/main/StudyInvite.scala +++ b/modules/study/src/main/StudyInvite.scala @@ -5,7 +5,7 @@ import lila.notify.{ InvitedToStudy, NotifyApi } import lila.pref.Pref import lila.relation.{ Block, Follow } import lila.security.Granter -import lila.user.{ Me, User } +import lila.user.{ Me, User, MyId } final private class StudyInvite( studyRepo: StudyRepo, @@ -74,12 +74,12 @@ final private class StudyInvite( .void yield invited - def admin(study: Study)(using me: Me): Funit = + def becomeAdmin(me: MyId)(study: Study): Funit = studyRepo.coll: _.update .one( $id(study.id), - $set(s"members.${me.userId}" -> $doc("role" -> "w", "admin" -> true)) ++ - $addToSet("uids" -> me.userId) + $set(s"members.${me}" -> $doc("role" -> "w", "admin" -> true)) ++ + $addToSet("uids" -> me) ) .void diff --git a/modules/study/src/main/StudySocket.scala b/modules/study/src/main/StudySocket.scala index e6233832a21ac..3526cf0ec32fb 100644 --- a/modules/study/src/main/StudySocket.scala +++ b/modules/study/src/main/StudySocket.scala @@ -14,6 +14,7 @@ import lila.socket.Socket.{ makeMessage, Sri } import lila.socket.{ AnaAny, AnaDests, AnaDrop, AnaMove } import lila.tree.Node.{ defaultNodeJsonWriter, Comment, Gamebook, Shape, Shapes } import lila.tree.Branch +import lila.user.MyId final private class StudySocket( api: StudyApi, @@ -113,11 +114,13 @@ final private class StudySocket( case "kick" => o.get[UserStr]("d") .foreach: username => - applyWho(api.kick(studyId, username.id)) + applyWho: w => + api.kick(studyId, username.id, w.myId) + Bus.publish(actorApi.Kick(studyId, username.id, w.myId), "kickStudy") case "leave" => who.foreach: w => - api.kick(studyId, w.u)(w) + api.kick(studyId, w.u, w.myId) case "shapes" => reading[AtPosition](o): position => @@ -237,9 +240,8 @@ final private class StudySocket( ) case "relaySync" => - who.foreach(w => + applyWho: w => Bus.publish(actorApi.RelayToggle(studyId, ~(o \ "d").asOpt[Boolean], w), "relayToggle") - ) case t => logger.warn(s"Unhandled study socket message: $t") diff --git a/modules/study/src/main/actorApi.scala b/modules/study/src/main/actorApi.scala index 0ff2871d1006e..63319541a16d3 100644 --- a/modules/study/src/main/actorApi.scala +++ b/modules/study/src/main/actorApi.scala @@ -2,6 +2,7 @@ package lila.study package actorApi import chess.format.UciPath +import lila.user.{ Me, MyId } case class StartStudy(studyId: StudyId) case class SaveStudy(study: Study) @@ -11,6 +12,9 @@ case class ExplorerGame(ch: StudyChapterId, path: UciPath, gameId: GameId, inser def chapterId = ch val position = Position.Ref(chapterId, path) -case class Who(u: UserId, sri: lila.socket.Socket.Sri) +case class Who(u: UserId, sri: lila.socket.Socket.Sri): + def myId = u into MyId case class RelayToggle(studyId: StudyId, v: Boolean, who: Who) +case class Kick(studyId: StudyId, userId: UserId, who: MyId) +case class BecomeStudyAdmin(studyId: StudyId, me: Me) case class IsOfficialRelay(studyId: StudyId, promise: Promise[Boolean])