configurable swiss chat
parent
0a88df36c5
commit
516add7881
|
@ -9,6 +9,7 @@ import lila.chat.Chat
|
|||
import lila.common.HTTPRequest
|
||||
import lila.game.{ Pov, Game => GameModel, PgnDump }
|
||||
import lila.tournament.{ Tournament => Tour }
|
||||
import lila.swiss.Swiss.{ Id => SwissId }
|
||||
import lila.user.{ User => UserModel }
|
||||
import views._
|
||||
|
||||
|
@ -17,7 +18,8 @@ final class Round(
|
|||
gameC: => Game,
|
||||
challengeC: => Challenge,
|
||||
analyseC: => Analyse,
|
||||
tournamentC: => Tournament
|
||||
tournamentC: => Tournament,
|
||||
swissC: => Swiss
|
||||
) extends LilaController(env)
|
||||
with TheftPrevention {
|
||||
|
||||
|
@ -247,7 +249,14 @@ final class Round(
|
|||
case (_, Some(sid), _) =>
|
||||
env.chat.api.userChat.cached.findMine(Chat.Id(sid), ctx.me).dmap(toEventChat(s"simul/$sid"))
|
||||
case (_, _, Some(sid)) =>
|
||||
env.chat.api.userChat.cached.findMine(Chat.Id(sid), ctx.me).dmap(toEventChat(s"swiss/$sid"))
|
||||
env.swiss.api
|
||||
.roundInfo(SwissId(sid))
|
||||
.flatMap { _ ?? swissC.canHaveChat }
|
||||
.flatMap {
|
||||
_ ?? {
|
||||
env.chat.api.userChat.cached.findMine(Chat.Id(sid), ctx.me).dmap(toEventChat(s"swiss/$sid"))
|
||||
}
|
||||
}
|
||||
case _ =>
|
||||
game.hasChat ?? {
|
||||
env.chat.api.playerChat.findIf(Chat.Id(game.id), !game.justCreated) map { chat =>
|
||||
|
|
|
@ -6,7 +6,7 @@ import scala.concurrent.duration._
|
|||
|
||||
import lila.api.Context
|
||||
import lila.app._
|
||||
import lila.swiss.Swiss.{ Id => SwissId }
|
||||
import lila.swiss.Swiss.{ Id => SwissId, ChatFor }
|
||||
import lila.swiss.{ Swiss => SwissModel }
|
||||
import views._
|
||||
|
||||
|
@ -81,7 +81,7 @@ final class Swiss(
|
|||
|
||||
def create(teamId: String) =
|
||||
AuthBody { implicit ctx => me =>
|
||||
env.team.teamRepo.isLeader(teamId, me.id) flatMap {
|
||||
env.team.cached.isLeader(teamId, me.id) flatMap {
|
||||
case false => notFound
|
||||
case _ =>
|
||||
env.swiss.forms.create
|
||||
|
@ -102,7 +102,7 @@ final class Swiss(
|
|||
ScopedBody() { implicit req => me =>
|
||||
if (me.isBot || me.lame) notFoundJson("This account cannot create tournaments")
|
||||
else
|
||||
env.team.teamRepo.isLeader(teamId, me.id) flatMap {
|
||||
env.team.cached.isLeader(teamId, me.id) flatMap {
|
||||
case false => notFoundJson("You're not a leader of that team")
|
||||
case _ =>
|
||||
env.swiss.forms.create.bindFromRequest
|
||||
|
@ -251,7 +251,13 @@ final class Swiss(
|
|||
}
|
||||
|
||||
private def canHaveChat(swiss: SwissModel)(implicit ctx: Context): Fu[Boolean] =
|
||||
(swiss.settings.hasChat && ctx.noKid) ?? ctx.userId.?? {
|
||||
env.team.api.belongsTo(swiss.teamId, _)
|
||||
canHaveChat(swiss.roundInfo)
|
||||
|
||||
private[controllers] def canHaveChat(swiss: SwissModel.RoundInfo)(implicit ctx: Context): Fu[Boolean] =
|
||||
swiss.chatFor match {
|
||||
case ChatFor.NONE => fuFalse
|
||||
case ChatFor.LEADERS => ctx.userId ?? { env.team.cached.isLeader(swiss.teamId, _) }
|
||||
case ChatFor.MEMBERS => ctx.userId ?? { env.team.api.belongsTo(swiss.teamId, _) }
|
||||
case _ => fuTrue
|
||||
}
|
||||
}
|
||||
|
|
|
@ -195,7 +195,7 @@ final class Tournament(
|
|||
val teamId = ctx.body.body.\("team").asOpt[String]
|
||||
teamId
|
||||
.?? {
|
||||
env.team.teamRepo.isLeader(_, me.id)
|
||||
env.team.cached.isLeader(_, me.id)
|
||||
}
|
||||
.flatMap { isLeader =>
|
||||
api.joinWithResult(id, me, password, teamId, getUserTeamIds, isLeader) flatMap { result =>
|
||||
|
|
|
@ -34,6 +34,9 @@ object form {
|
|||
fields.roundInterval,
|
||||
fields.startsAt
|
||||
),
|
||||
form3.split(
|
||||
fields.chatFor
|
||||
),
|
||||
form3.globalError(form),
|
||||
form3.actions(
|
||||
a(href := routes.Team.show(teamId))(trans.cancel()),
|
||||
|
@ -66,6 +69,9 @@ object form {
|
|||
fields.roundInterval,
|
||||
swiss.isCreated option fields.startsAt
|
||||
),
|
||||
form3.split(
|
||||
fields.chatFor
|
||||
),
|
||||
form3.globalError(form),
|
||||
form3.actions(
|
||||
a(href := routes.Swiss.show(swiss.id.value))(trans.cancel()),
|
||||
|
@ -99,7 +105,12 @@ final private class SwissFields(form: Form[_])(implicit ctx: Context) {
|
|||
)
|
||||
}
|
||||
def nbRounds =
|
||||
form3.group(form("nbRounds"), "Number of rounds", half = true)(
|
||||
form3.group(
|
||||
form("nbRounds"),
|
||||
"Number of rounds",
|
||||
help = raw("An odd number of rounds allows optimal color balance.").some,
|
||||
half = true
|
||||
)(
|
||||
form3.input(_, typ = "number")
|
||||
)
|
||||
|
||||
|
@ -141,4 +152,17 @@ final private class SwissFields(form: Form[_])(implicit ctx: Context) {
|
|||
frag("Tournament start date"),
|
||||
half = true
|
||||
)(form3.flatpickr(_))
|
||||
|
||||
def chatFor =
|
||||
form3.group(form("chatFor"), frag("Tournament chat"), half = true) { f =>
|
||||
form3.select(
|
||||
f,
|
||||
Seq(
|
||||
Swiss.ChatFor.NONE -> "No chat",
|
||||
Swiss.ChatFor.LEADERS -> "Only team leaders",
|
||||
Swiss.ChatFor.MEMBERS -> "Only team members",
|
||||
Swiss.ChatFor.ALL -> "All Lichess players"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,9 +68,9 @@ object form {
|
|||
form3.select(
|
||||
f,
|
||||
Seq(
|
||||
Team.ChatFor.NONE -> "Disabled",
|
||||
Team.ChatFor.LEADERS -> "Leaders only",
|
||||
Team.ChatFor.MEMBERS -> "All members"
|
||||
Team.ChatFor.NONE -> "No chat",
|
||||
Team.ChatFor.LEADERS -> "Team leaders",
|
||||
Team.ChatFor.MEMBERS -> "Team members"
|
||||
)
|
||||
)
|
||||
},
|
||||
|
|
|
@ -127,7 +127,7 @@ private object BsonHandlers {
|
|||
nbRounds = r.get[Int]("n"),
|
||||
rated = r.boolO("r") | true,
|
||||
description = r.strO("d"),
|
||||
hasChat = r.boolO("c") | true,
|
||||
chatFor = r.intO("c") | Swiss.ChatFor.default,
|
||||
roundInterval = (r.intO("i") | 60).seconds
|
||||
)
|
||||
def writes(w: BSON.Writer, s: Swiss.Settings) =
|
||||
|
@ -135,7 +135,7 @@ private object BsonHandlers {
|
|||
"n" -> s.nbRounds,
|
||||
"r" -> (!s.rated).option(false),
|
||||
"d" -> s.description,
|
||||
"c" -> (!s.hasChat).option(false),
|
||||
"c" -> (s.chatFor != Swiss.ChatFor.default).option(s.chatFor),
|
||||
"i" -> s.roundInterval.toSeconds.toInt
|
||||
)
|
||||
}
|
||||
|
|
|
@ -64,6 +64,8 @@ case class Swiss(
|
|||
if (minutes < 60) s"${minutes}m"
|
||||
else s"${minutes / 60}h" + (if (minutes % 60 != 0) s" ${(minutes % 60)}m" else "")
|
||||
}
|
||||
|
||||
def roundInfo = Swiss.RoundInfo(teamId, settings.chatFor)
|
||||
}
|
||||
|
||||
object Swiss {
|
||||
|
@ -85,7 +87,7 @@ object Swiss {
|
|||
nbRounds: Int,
|
||||
rated: Boolean,
|
||||
description: Option[String] = None,
|
||||
hasChat: Boolean = true,
|
||||
chatFor: ChatFor = ChatFor.default,
|
||||
roundInterval: FiniteDuration
|
||||
) {
|
||||
lazy val intervalSeconds = roundInterval.toSeconds.toInt
|
||||
|
@ -93,6 +95,15 @@ object Swiss {
|
|||
def dailyInterval = (!manualRounds && intervalSeconds >= 24 * 3600) option intervalSeconds / 3600 / 24
|
||||
}
|
||||
|
||||
type ChatFor = Int
|
||||
object ChatFor {
|
||||
val NONE = 0
|
||||
val LEADERS = 10
|
||||
val MEMBERS = 20
|
||||
val ALL = 30
|
||||
val default = MEMBERS
|
||||
}
|
||||
|
||||
object RoundInterval {
|
||||
val auto = -1
|
||||
val manual = 99999999
|
||||
|
@ -106,4 +117,9 @@ object Swiss {
|
|||
def makeId = Id(scala.util.Random.alphanumeric take 8 mkString)
|
||||
|
||||
case class PastAndNext(past: List[Swiss], next: List[Swiss])
|
||||
|
||||
case class RoundInfo(
|
||||
teamId: TeamID,
|
||||
chatFor: ChatFor
|
||||
)
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@ final class SwissApi(
|
|||
nbRounds = data.nbRounds,
|
||||
rated = data.rated | true,
|
||||
description = data.description,
|
||||
hasChat = data.hasChat | true,
|
||||
chatFor = data.realChatFor,
|
||||
roundInterval = data.realRoundInterval
|
||||
)
|
||||
)
|
||||
|
@ -83,37 +83,37 @@ final class SwissApi(
|
|||
|
||||
def update(swiss: Swiss, data: SwissForm.SwissData): Funit =
|
||||
Sequencing(swiss.id)(byId) { old =>
|
||||
colls.swiss.update
|
||||
.one(
|
||||
$id(old.id),
|
||||
old.copy(
|
||||
name = data.name | old.name,
|
||||
clock = data.clock,
|
||||
variant = data.realVariant,
|
||||
startsAt = data.startsAt.ifTrue(old.isCreated) | old.startsAt,
|
||||
nextRoundAt =
|
||||
if (old.isCreated) Some(data.startsAt | old.startsAt)
|
||||
else old.nextRoundAt,
|
||||
settings = old.settings.copy(
|
||||
nbRounds = data.nbRounds,
|
||||
rated = data.rated | old.settings.rated,
|
||||
description = data.description,
|
||||
hasChat = data.hasChat | old.settings.hasChat,
|
||||
roundInterval =
|
||||
if (data.roundInterval.isDefined) data.realRoundInterval
|
||||
else old.settings.roundInterval
|
||||
)
|
||||
) pipe { s =>
|
||||
if (
|
||||
s.isStarted && s.nbOngoing == 0 && (s.nextRoundAt.isEmpty || old.settings.manualRounds) && !s.settings.manualRounds
|
||||
)
|
||||
s.copy(nextRoundAt = DateTime.now.plusSeconds(s.settings.roundInterval.toSeconds.toInt).some)
|
||||
else if (s.settings.manualRounds && !old.settings.manualRounds)
|
||||
s.copy(nextRoundAt = none)
|
||||
else s
|
||||
} pipe addFeaturable
|
||||
)
|
||||
.void >>- socket.reload(swiss.id)
|
||||
val swiss =
|
||||
old.copy(
|
||||
name = data.name | old.name,
|
||||
clock = data.clock,
|
||||
variant = data.realVariant,
|
||||
startsAt = data.startsAt.ifTrue(old.isCreated) | old.startsAt,
|
||||
nextRoundAt =
|
||||
if (old.isCreated) Some(data.startsAt | old.startsAt)
|
||||
else old.nextRoundAt,
|
||||
settings = old.settings.copy(
|
||||
nbRounds = data.nbRounds,
|
||||
rated = data.rated | old.settings.rated,
|
||||
description = data.description orElse old.settings.description,
|
||||
chatFor = data.chatFor | old.settings.chatFor,
|
||||
roundInterval =
|
||||
if (data.roundInterval.isDefined) data.realRoundInterval
|
||||
else old.settings.roundInterval
|
||||
)
|
||||
) pipe { s =>
|
||||
if (
|
||||
s.isStarted && s.nbOngoing == 0 && (s.nextRoundAt.isEmpty || old.settings.manualRounds) && !s.settings.manualRounds
|
||||
)
|
||||
s.copy(nextRoundAt = DateTime.now.plusSeconds(s.settings.roundInterval.toSeconds.toInt).some)
|
||||
else if (s.settings.manualRounds && !old.settings.manualRounds)
|
||||
s.copy(nextRoundAt = none)
|
||||
else s
|
||||
}
|
||||
colls.swiss.update.one($id(old.id), addFeaturable(swiss)).void >>- {
|
||||
cache.roundInfo.put(swiss.id, fuccess(swiss.roundInfo.some))
|
||||
socket.reload(swiss.id)
|
||||
}
|
||||
}
|
||||
|
||||
def scheduleNextRound(swiss: Swiss, date: DateTime): Funit =
|
||||
|
@ -384,6 +384,8 @@ final class SwissApi(
|
|||
else funit
|
||||
} >>- cache.featuredInTeam.invalidate(swiss.teamId)
|
||||
|
||||
def roundInfo = cache.roundInfo.get _
|
||||
|
||||
private def recomputeAndUpdateAll(id: Swiss.Id): Funit =
|
||||
scoring(id).flatMap {
|
||||
_ ?? { res =>
|
||||
|
|
|
@ -24,6 +24,13 @@ final private class SwissCache(
|
|||
expireAfter = Syncache.ExpireAfterAccess(20 minutes)
|
||||
)
|
||||
|
||||
val roundInfo = cacheApi[Swiss.Id, Option[Swiss.RoundInfo]](32, "swiss.roundInfo") {
|
||||
_.expireAfterWrite(1 minute)
|
||||
.buildAsyncFuture { id =>
|
||||
colls.swiss.byId[Swiss](id.value).map2(_.roundInfo)
|
||||
}
|
||||
}
|
||||
|
||||
private[swiss] object featuredInTeam {
|
||||
private val compute = (teamId: TeamID) => {
|
||||
val max = 5
|
||||
|
|
|
@ -38,7 +38,7 @@ final class SwissForm(implicit mode: Mode) {
|
|||
"rated" -> optional(boolean),
|
||||
"nbRounds" -> number(min = minRounds, max = 100),
|
||||
"description" -> optional(nonEmptyText),
|
||||
"hasChat" -> optional(boolean),
|
||||
"chatFor" -> optional(numberIn(chatForChoices.map(_._1))),
|
||||
"roundInterval" -> optional(numberIn(roundIntervals))
|
||||
)(SwissData.apply)(SwissData.unapply)
|
||||
)
|
||||
|
@ -52,9 +52,9 @@ final class SwissForm(implicit mode: Mode) {
|
|||
}),
|
||||
variant = Variant.default.key.some,
|
||||
rated = true.some,
|
||||
nbRounds = 8,
|
||||
nbRounds = 7,
|
||||
description = none,
|
||||
hasChat = true.some,
|
||||
chatFor = Swiss.ChatFor.default.some,
|
||||
roundInterval = Swiss.RoundInterval.auto.some
|
||||
)
|
||||
|
||||
|
@ -67,7 +67,7 @@ final class SwissForm(implicit mode: Mode) {
|
|||
rated = s.settings.rated.some,
|
||||
nbRounds = s.settings.nbRounds,
|
||||
description = s.settings.description,
|
||||
hasChat = s.settings.hasChat.some,
|
||||
chatFor = s.settings.chatFor.some,
|
||||
roundInterval = s.settings.roundInterval.toSeconds.toInt.some
|
||||
)
|
||||
|
||||
|
@ -118,7 +118,7 @@ object SwissForm {
|
|||
val roundIntervalChoices = options(
|
||||
roundIntervals,
|
||||
s =>
|
||||
if (s == Swiss.RoundInterval.auto) s"Automatic (recommended)"
|
||||
if (s == Swiss.RoundInterval.auto) s"Automatic"
|
||||
else if (s == Swiss.RoundInterval.manual) s"Manually schedule each round"
|
||||
else if (s < 60) s"$s seconds"
|
||||
else if (s < 3600) s"${s / 60} minute(s)"
|
||||
|
@ -126,6 +126,13 @@ object SwissForm {
|
|||
else s"${s / 24 / 3600} days(s)"
|
||||
)
|
||||
|
||||
val chatForChoices = List(
|
||||
Swiss.ChatFor.NONE -> "No chat",
|
||||
Swiss.ChatFor.LEADERS -> "Team leaders only",
|
||||
Swiss.ChatFor.MEMBERS -> "Team members only",
|
||||
Swiss.ChatFor.ALL -> "All Lichess players"
|
||||
)
|
||||
|
||||
case class SwissData(
|
||||
name: Option[String],
|
||||
clock: ClockConfig,
|
||||
|
@ -134,11 +141,12 @@ object SwissForm {
|
|||
rated: Option[Boolean],
|
||||
nbRounds: Int,
|
||||
description: Option[String],
|
||||
hasChat: Option[Boolean],
|
||||
chatFor: Option[Int],
|
||||
roundInterval: Option[Int]
|
||||
) {
|
||||
def realVariant = variant flatMap Variant.apply getOrElse Variant.default
|
||||
def realStartsAt = startsAt | DateTime.now.plusMinutes(10)
|
||||
def realChatFor = chatFor | Swiss.ChatFor.default
|
||||
def realRoundInterval = {
|
||||
(roundInterval | Swiss.RoundInterval.auto) match {
|
||||
case Swiss.RoundInterval.auto =>
|
||||
|
|
|
@ -49,4 +49,12 @@ final class Cached(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
val leaders = cacheApi[Team.ID, Set[User.ID]](32, "team.leaders") {
|
||||
_.expireAfterWrite(1 minute)
|
||||
.buildAsyncFuture(teamRepo.leadersOf)
|
||||
}
|
||||
|
||||
def isLeader(teamId: Team.ID, userId: User.ID): Fu[Boolean] =
|
||||
leaders.get(teamId).dmap(_ contains userId)
|
||||
}
|
||||
|
|
|
@ -53,6 +53,6 @@ final class Env(
|
|||
lila.common.Bus.subscribeFun("shadowban", "teamIsLeader") {
|
||||
case lila.hub.actorApi.mod.Shadowban(userId, true) => api deleteRequestsByUserId userId
|
||||
case lila.hub.actorApi.team.IsLeader(teamId, userId, promise) =>
|
||||
promise completeWith teamRepo.isLeader(teamId, userId)
|
||||
promise completeWith cached.isLeader(teamId, userId)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -217,7 +217,10 @@ final class TeamApi(
|
|||
}
|
||||
} getOrElse Set.empty
|
||||
memberRepo.filterUserIdsInTeam(team.id, leaders) flatMap { ids =>
|
||||
ids.nonEmpty ?? teamRepo.setLeaders(team.id, ids).void
|
||||
ids.nonEmpty ?? {
|
||||
cached.leaders.put(team.id, fuccess(ids))
|
||||
teamRepo.setLeaders(team.id, ids).void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -41,9 +41,6 @@ final class TeamRepo(val coll: Coll)(implicit ec: scala.concurrent.ExecutionCont
|
|||
def leadersOf(teamId: Team.ID): Fu[Set[User.ID]] =
|
||||
coll.primitiveOne[Set[User.ID]]($id(teamId), "leaders").dmap(~_)
|
||||
|
||||
def isLeader(teamId: Team.ID, userId: User.ID): Fu[Boolean] =
|
||||
coll.exists($id(teamId) ++ $doc("leaders" -> userId))
|
||||
|
||||
def setLeaders(teamId: String, leaders: Set[User.ID]) =
|
||||
coll.updateField($id(teamId), "leaders", leaders)
|
||||
|
||||
|
|
|
@ -6,7 +6,8 @@ import lila.socket.RemoteSocket.{ Protocol => P, _ }
|
|||
final private class TeamSocket(
|
||||
remoteSocketApi: lila.socket.RemoteSocket,
|
||||
chat: lila.chat.ChatApi,
|
||||
teamRepo: TeamRepo
|
||||
teamRepo: TeamRepo,
|
||||
cached: Cached
|
||||
)(implicit ec: scala.concurrent.ExecutionContext, mode: play.api.Mode) {
|
||||
|
||||
lazy val rooms = makeRoomMap(send)
|
||||
|
@ -20,7 +21,7 @@ final private class TeamSocket(
|
|||
logger,
|
||||
roomId => _.Team(roomId.value).some,
|
||||
localTimeout = Some { (roomId, modId, suspectId) =>
|
||||
teamRepo.isLeader(roomId.value, modId) >>& !teamRepo.isLeader(roomId.value, suspectId)
|
||||
cached.isLeader(roomId.value, modId) >>& !cached.isLeader(roomId.value, suspectId)
|
||||
},
|
||||
chatBusChan = _.Team
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue