configurable swiss chat

user-mod-page
Thibault Duplessis 2020-05-23 21:18:43 -06:00
parent 0a88df36c5
commit 516add7881
15 changed files with 141 additions and 60 deletions

View File

@ -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 =>

View File

@ -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
}
}

View File

@ -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 =>

View File

@ -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"
)
)
}
}

View File

@ -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"
)
)
},

View File

@ -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
)
}

View File

@ -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
)
}

View File

@ -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 =>

View File

@ -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

View File

@ -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 =>

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -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)

View File

@ -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
)