lila/app/controllers/Tournament.scala

449 lines
15 KiB
Scala
Raw Normal View History

2013-05-07 17:44:26 -06:00
package controllers
2015-01-04 08:43:17 -07:00
import play.api.libs.json._
2013-07-30 15:02:12 -06:00
import play.api.mvc._
2020-04-29 08:58:36 -06:00
import scala.annotation.nowarn
2017-04-07 01:39:15 -06:00
import scala.concurrent.duration._
2013-07-30 15:02:12 -06:00
2013-12-29 02:51:40 -07:00
import lila.api.Context
2013-07-30 15:02:12 -06:00
import lila.app._
2017-10-19 22:02:55 -06:00
import lila.chat.Chat
2019-12-08 11:12:00 -07:00
import lila.common.HTTPRequest
2019-12-08 09:58:50 -07:00
import lila.hub.LightTeam._
2019-12-23 18:01:45 -07:00
import lila.memo.CacheApi._
2019-12-08 11:12:00 -07:00
import lila.tournament.{ VisibleTournaments, Tournament => Tour }
2017-08-16 18:39:52 -06:00
import lila.user.{ User => UserModel }
2013-12-29 02:51:40 -07:00
import views._
2013-05-07 17:44:26 -06:00
2019-12-04 23:52:53 -07:00
final class Tournament(
env: Env,
2019-12-05 14:51:18 -07:00
teamC: => Team
2019-12-04 23:52:53 -07:00
) extends LilaController(env) {
2013-05-07 17:44:26 -06:00
2019-12-13 07:30:20 -07:00
private def repo = env.tournament.tournamentRepo
private def api = env.tournament.api
2019-12-04 23:52:53 -07:00
private def jsonView = env.tournament.jsonView
2019-12-13 07:30:20 -07:00
private def forms = env.tournament.forms
2013-05-12 09:02:45 -06:00
2019-03-04 01:57:30 -07:00
private def tournamentNotFound(implicit ctx: Context) = NotFound(html.tournament.bits.notFound())
2013-05-12 16:48:48 -06:00
2019-12-23 18:01:45 -07:00
private[controllers] val upcomingCache = env.memo.cacheApi.unit[(VisibleTournaments, List[Tour])] {
2020-04-29 08:58:36 -06:00
_.refreshAfterWrite(3.seconds)
2019-12-23 18:01:45 -07:00
.buildAsyncFuture { _ =>
for {
visible <- api.fetchVisibleTournaments
scheduled <- repo.scheduledDedup
} yield (visible, scheduled)
}
}
2017-04-07 01:39:15 -06:00
def home = Open { implicit ctx =>
2016-01-25 19:59:18 -07:00
negotiate(
html = for {
(visible, scheduled) <- upcomingCache.getUnit
finished <- api.notableFinished
winners <- env.tournament.winners.all
scheduleJson <- env.tournament apiJsonView visible
} yield NoCache {
2018-03-27 18:11:49 -06:00
pageHit
Ok(html.tournament.home(scheduled, finished, winners, scheduleJson))
2016-01-25 19:59:18 -07:00
},
2019-12-13 07:30:20 -07:00
api = _ =>
for {
2019-12-23 18:01:45 -07:00
(visible, _) <- upcomingCache.getUnit
2019-12-13 07:30:20 -07:00
scheduleJson <- env.tournament apiJsonView visible
} yield Ok(scheduleJson)
2016-01-25 19:59:18 -07:00
)
2013-05-12 09:02:45 -06:00
}
2020-04-29 08:58:36 -06:00
def help(@nowarn("cat=unused") sysStr: Option[String]) = Open { implicit ctx =>
2019-12-04 23:52:53 -07:00
Ok(html.tournament.faq.page).fuccess
2014-07-23 15:08:59 -06:00
}
2013-05-12 09:02:45 -06:00
2016-10-17 08:17:55 -06:00
def leaderboard = Open { implicit ctx =>
2017-01-26 05:19:27 -07:00
for {
2019-12-04 23:52:53 -07:00
winners <- env.tournament.winners.all
2019-12-13 07:30:20 -07:00
_ <- env.user.lightUserApi preloadMany winners.userIds
2017-01-26 05:19:27 -07:00
} yield Ok(html.tournament.leaderboard(winners))
2016-10-17 08:17:55 -06:00
}
private[controllers] def canHaveChat(tour: Tour, json: Option[JsObject])(implicit ctx: Context): Boolean =
tour.hasChat && !ctx.kid && // no public chats for kids
2020-04-06 07:59:29 -06:00
ctx.me.fold(!tour.isPrivate) { u => // anon can see public chats, except for private tournaments
(!tour.isPrivate || json.fold(true)(jsonHasMe) || isGranted(_.ChatTimeout)) && // private tournament that I joined or has ChatTimeout
2019-12-13 07:30:20 -07:00
env.chat.panic.allowed(u, tighter = tour.variant == chess.variant.Antichess)
2019-05-02 19:54:52 -06:00
}
2017-08-16 18:39:52 -06:00
private def jsonHasMe(js: JsObject): Boolean = (js \ "me").toOption.isDefined
2014-02-17 02:12:19 -07:00
def show(id: String) = Open { implicit ctx =>
val page = getInt("page")
2018-12-04 21:33:19 -07:00
repo byId id flatMap { tourOption =>
negotiate(
2019-12-13 07:30:20 -07:00
html = tourOption
.fold(tournamentNotFound.fuccess) { tour =>
(for {
verdicts <- api.verdicts(tour, ctx.me, getUserTeamIds)
version <- env.tournament.version(tour.id)
json <- jsonView(
tour = tour,
page = page,
me = ctx.me,
getUserTeamIds = getUserTeamIds,
getTeamName = env.team.getTeamName,
playerInfoExt = none,
socketVersion = version.some,
partial = false
2019-12-13 07:30:20 -07:00
)
chat <- canHaveChat(tour, json.some) ?? env.chat.api.userChat.cached
.findMine(Chat.Id(tour.id), ctx.me)
2020-04-29 12:57:13 -06:00
.dmap(some)
2019-12-13 07:30:20 -07:00
_ <- chat ?? { c =>
env.user.lightUserApi.preloadMany(c.chat.userIds)
}
_ <- tour.teamBattle ?? { b =>
env.team.cached.preloadSet(b.teams)
}
streamers <- streamerCache get tour.id
shieldOwner <- env.tournament.shieldApi currentOwner tour
} yield Ok(html.tournament.show(tour, verdicts, json, chat, streamers, shieldOwner)))
}
.monSuccess(_.tournament.apiShowPartial(false, HTTPRequest clientName ctx.req)),
api = _ =>
tourOption
.fold(notFoundJson("No such tournament")) { tour =>
get("playerInfo").?? { api.playerInfo(tour, _) } zip
2020-04-30 09:19:34 -06:00
getBool("socketVersion").??(env.tournament version tour.id dmap some) flatMap {
2019-12-13 07:30:20 -07:00
case (playerInfoExt, socketVersion) =>
jsonView(
tour = tour,
page = page,
me = ctx.me,
getUserTeamIds = getUserTeamIds,
getTeamName = env.team.getTeamName,
playerInfoExt = playerInfoExt,
socketVersion = socketVersion,
2020-04-30 09:19:34 -06:00
partial = getBool("partial")
2019-12-13 07:30:20 -07:00
)
} dmap { Ok(_) }
}
.monSuccess(_.tournament.apiShowPartial(getBool("partial"), HTTPRequest clientName ctx.req))
2019-12-07 23:36:14 -07:00
) dmap NoCache
2018-12-04 21:33:19 -07:00
}
}
def standing(id: String, page: Int) = Open { implicit ctx =>
OptionFuResult(repo byId id) { tour =>
2020-05-04 15:16:36 -06:00
JsonOk {
env.tournament.standingApi(tour, page)
2013-05-12 09:02:45 -06:00
}
}
}
2018-01-08 11:03:17 -07:00
def pageOf(id: String, userId: String) = Open { implicit ctx =>
OptionFuResult(repo byId id) { tour =>
2019-12-04 23:52:53 -07:00
api.pageOf(tour, UserModel normalize userId) flatMap {
2018-01-08 11:03:17 -07:00
_ ?? { page =>
2019-12-15 18:53:17 -07:00
env.tournament.standingApi(tour, page) map { data =>
2018-01-08 11:03:17 -07:00
Ok(data) as JSON
}
}
}
}
}
2020-05-05 16:18:58 -06:00
def player(tourId: String, userId: String) = Action.async {
2019-12-04 23:52:53 -07:00
env.tournament.tournamentRepo byId tourId flatMap {
2019-10-04 14:46:43 -06:00
_ ?? { tour =>
JsonOk {
2019-12-04 23:52:53 -07:00
api.playerInfo(tour, userId) flatMap {
_ ?? { jsonView.playerInfoExtended(tour, _) }
2019-10-04 14:46:43 -06:00
}
}
}
}
}
def teamInfo(tourId: String, teamId: TeamID) = Open { _ =>
2019-12-04 23:52:53 -07:00
env.tournament.tournamentRepo byId tourId flatMap {
2019-10-04 14:46:43 -06:00
_ ?? { tour =>
2019-12-04 23:52:53 -07:00
jsonView.teamInfo(tour, teamId) map {
2019-10-05 13:52:28 -06:00
_ ?? { json =>
Ok(json) as JSON
2019-10-04 14:46:43 -06:00
}
}
2015-10-02 14:52:00 -06:00
}
}
}
2019-12-04 23:52:53 -07:00
def join(id: String) = AuthBody(parse.json) { implicit ctx => implicit me =>
2018-04-17 17:15:58 -06:00
NoLameOrBot {
2017-10-19 22:02:55 -06:00
NoPlayban {
val password = ctx.body.body.\("p").asOpt[String]
2019-12-13 07:30:20 -07:00
val teamId = ctx.body.body.\("team").asOpt[String]
2019-12-04 23:52:53 -07:00
api.joinWithResult(id, me, password, teamId, getUserTeamIds) flatMap { result =>
negotiate(
2018-08-25 04:47:59 -06:00
html = Redirect(routes.Tournament.show(id)).fuccess,
2019-12-13 07:30:20 -07:00
api = _ =>
fuccess {
if (result) jsonOkResult
else BadRequest(Json.obj("joined" -> false))
}
2018-08-25 04:47:59 -06:00
)
}
2017-10-19 22:02:55 -06:00
}
}
2013-05-12 09:02:45 -06:00
}
2018-04-04 17:54:13 -06:00
def pause(id: String) = Auth { implicit ctx => me =>
OptionResult(repo byId id) { tour =>
2019-12-04 23:52:53 -07:00
api.selfPause(tour.id, me.id)
if (HTTPRequest.isXhr(ctx.req)) jsonOkResult
else Redirect(routes.Tournament.show(tour.id))
}
2013-05-12 09:02:45 -06:00
}
def form = Auth { implicit ctx => me =>
2018-04-17 17:15:58 -06:00
NoLameOrBot {
2019-12-05 14:51:18 -07:00
teamC.teamsIBelongTo(me) map { teams =>
2020-04-06 08:06:24 -06:00
Ok(html.tournament.form.create(forms.create(me), teams))
2019-10-02 04:20:44 -06:00
}
}
}
def teamBattleForm(teamId: TeamID) = Auth { implicit ctx => me =>
2019-10-02 04:20:44 -06:00
NoLameOrBot {
env.team.api.leads(teamId, me.id) map {
2019-10-02 04:20:44 -06:00
_ ?? {
2020-04-06 08:06:24 -06:00
Ok(html.tournament.form.create(forms.create(me, teamId.some), Nil))
2019-10-02 04:20:44 -06:00
}
}
}
2013-05-12 09:02:45 -06:00
}
2018-03-25 06:57:23 -06:00
private val CreateLimitPerUser = new lila.memo.RateLimit[lila.user.User.ID](
credits = 240,
2020-04-29 08:58:36 -06:00
duration = 24.hour,
2018-03-25 06:57:23 -06:00
name = "tournament per user",
key = "tournament.user"
)
private val CreateLimitPerIP = new lila.memo.RateLimit[lila.common.IpAddress](
credits = 400,
2020-04-29 08:58:36 -06:00
duration = 24.hour,
2018-03-25 06:57:23 -06:00
name = "tournament per IP",
key = "tournament.ip"
)
2018-05-07 19:28:38 -06:00
private val rateLimited = ornicar.scalalib.Zero.instance[Fu[Result]] {
fuccess(Redirect(routes.Tournament.home))
2018-03-25 06:57:23 -06:00
}
2020-04-29 12:57:13 -06:00
private[controllers] def rateLimitCreation(me: UserModel, isPrivate: Boolean, req: RequestHeader)(
create: => Fu[Result]
): Fu[Result] = {
val cost =
if (me.id == "fide") 5
else if (me.hasTitle ||
env.streamer.liveStreamApi.isStreaming(me.id) ||
isGranted(_.ManageTournament, me) ||
me.isVerified ||
isPrivate) 10
else 20
CreateLimitPerUser(me.id, cost = cost) {
CreateLimitPerIP(HTTPRequest lastRemoteAddress req, cost = cost) {
create
}(rateLimited)
}(rateLimited)
}
2018-05-10 15:39:15 -06:00
def create = AuthBody { implicit ctx => me =>
2018-04-17 17:15:58 -06:00
NoLameOrBot {
2019-12-05 14:51:18 -07:00
teamC.teamsIBelongTo(me) flatMap { teams =>
implicit val req = ctx.body
negotiate(
html = forms
.create(me)
.bindFromRequest
.fold(
2020-04-06 08:06:24 -06:00
err => BadRequest(html.tournament.form.create(err, teams)).fuccess,
setup =>
rateLimitCreation(me, setup.isPrivate, ctx.req) {
api.createTournament(setup, me, teams, getUserTeamIds) map { tour =>
Redirect {
if (tour.isTeamBattle) routes.Tournament.teamBattleEdit(tour.id)
else routes.Tournament.show(tour.id)
}.flashSuccess
}
}
),
api = _ => doApiCreate(me)
)
}
}
2013-05-12 09:02:45 -06:00
}
2018-05-10 15:39:15 -06:00
def apiCreate = ScopedBody() { implicit req => me =>
2019-01-21 19:34:54 -07:00
if (me.isBot || me.lame) notFoundJson("This account cannot create tournaments")
else doApiCreate(me)
2018-05-10 15:39:15 -06:00
}
private def doApiCreate(me: lila.user.User)(implicit req: Request[_]): Fu[Result] =
forms
.create(me)
.bindFromRequest
.fold(
jsonFormErrorDefaultLang,
setup =>
rateLimitCreation(me, setup.isPrivate, req) {
teamC.teamsIBelongTo(me) flatMap { teams =>
api.createTournament(setup, me, teams, getUserTeamIds) flatMap { tour =>
jsonView(
tour,
none,
none,
getUserTeamIds,
env.team.getTeamName,
none,
none,
partial = false
)(reqLang) map { Ok(_) }
}
}
2019-12-13 07:30:20 -07:00
}
)
2018-05-10 15:39:15 -06:00
2019-10-02 10:50:09 -06:00
def teamBattleEdit(id: String) = Auth { implicit ctx => me =>
repo byId id flatMap {
_ ?? {
case tour if tour.createdBy == me.id =>
tour.teamBattle ?? { battle =>
2019-12-04 23:52:53 -07:00
env.team.teamRepo.byOrderedIds(battle.sortedTeamIds) flatMap { teams =>
2019-12-04 16:39:16 -07:00
env.user.lightUserApi.preloadMany(teams.map(_.createdBy)) >> {
2019-10-02 10:50:09 -06:00
val form = lila.tournament.TeamBattle.DataForm.edit(teams.map { t =>
2019-12-13 07:30:20 -07:00
s"""${t.id} "${t.name}" by ${env.user.lightUserApi
.sync(t.createdBy)
.fold(t.createdBy)(_.name)}"""
2019-10-05 13:52:28 -06:00
}, battle.nbLeaders)
2019-10-02 10:50:09 -06:00
Ok(html.tournament.teamBattle.edit(tour, form)).fuccess
}
}
}
case tour => Redirect(routes.Tournament.show(tour.id)).fuccess
}
}
}
def teamBattleUpdate(id: String) = AuthBody { implicit ctx => me =>
repo byId id flatMap {
_ ?? {
case tour if tour.createdBy == me.id && !tour.isFinished =>
implicit val req = ctx.body
2019-10-04 11:12:26 -06:00
lila.tournament.TeamBattle.DataForm.empty.bindFromRequest.fold(
2019-10-02 10:50:09 -06:00
err => BadRequest(html.tournament.teamBattle.edit(tour, err)).fuccess,
2019-12-13 07:30:20 -07:00
res =>
api.teamBattleUpdate(tour, res, env.team.api.filterExistingIds) inject
Redirect(routes.Tournament.show(tour.id))
2019-10-02 10:50:09 -06:00
)
case tour => Redirect(routes.Tournament.show(tour.id)).fuccess
}
}
}
def featured = Open { implicit ctx =>
negotiate(
html = notFound,
api = _ =>
2019-12-23 18:01:45 -07:00
env.tournament.cached.promotable.getUnit.nevermind map {
lila.tournament.Spotlight.select(_, ctx.me, 4)
2019-12-04 23:52:53 -07:00
} flatMap env.tournament.apiJsonView.featured map { Ok(_) }
)
}
2017-12-05 09:38:16 -07:00
def shields = Open { implicit ctx =>
for {
2019-12-04 23:52:53 -07:00
history <- env.tournament.shieldApi.history(5.some)
2019-12-13 07:30:20 -07:00
_ <- env.user.lightUserApi preloadMany history.userIds
2017-12-05 09:38:16 -07:00
} yield html.tournament.shields(history)
}
2017-12-11 08:51:09 -07:00
2019-04-30 19:04:42 -06:00
def categShields(k: String) = Open { implicit ctx =>
2019-12-04 23:52:53 -07:00
OptionFuOk(env.tournament.shieldApi.byCategKey(k)) {
2019-04-30 19:04:42 -06:00
case (categ, awards) =>
2019-12-04 16:39:16 -07:00
env.user.lightUserApi preloadMany awards.map(_.owner.value) inject
2019-04-30 19:04:42 -06:00
html.tournament.shields.byCateg(categ, awards)
}
}
2017-12-11 08:51:09 -07:00
def calendar = Open { implicit ctx =>
2019-12-04 23:52:53 -07:00
api.calendar map { tours =>
Ok(html.tournament.calendar(env.tournament.apiJsonView calendar tours))
2017-12-11 08:51:09 -07:00
}
}
2018-01-03 13:13:16 -07:00
2020-03-24 13:19:05 -06:00
def edit(id: String) = Auth { implicit ctx => me =>
WithEditableTournament(id, me) { tour =>
teamC.teamsIBelongTo(me) map { teams =>
2020-04-05 09:01:05 -06:00
val form = forms.edit(me, tour)
2020-04-06 08:06:24 -06:00
Ok(html.tournament.form.edit(tour, form, teams))
}
}
}
def update(id: String) = AuthBody { implicit ctx => me =>
WithEditableTournament(id, me) { tour =>
implicit val req = ctx.body
teamC.teamsIBelongTo(me) flatMap { teams =>
forms
2020-04-05 09:01:05 -06:00
.edit(me, tour)
.bindFromRequest
.fold(
2020-04-06 08:06:24 -06:00
err => BadRequest(html.tournament.form.edit(tour, err, teams)).fuccess,
data => api.update(tour, data, teams) inject Redirect(routes.Tournament.show(id)).flashSuccess
)
2020-03-24 13:19:05 -06:00
}
}
}
2020-04-02 15:44:12 -06:00
def terminate(id: String) = Auth { implicit ctx => me =>
2020-04-02 15:43:28 -06:00
WithEditableTournament(id, me) { tour =>
api kill tour inject {
env.mod.logApi.terminateTournament(me.id, tour.name())
Redirect(routes.Tournament.home)
2020-04-02 15:43:28 -06:00
}
}
}
private def WithEditableTournament(id: String, me: UserModel)(
f: Tour => Fu[Result]
)(implicit ctx: Context): Fu[Result] =
repo byId id flatMap {
case Some(t) if (t.createdBy == me.id && !t.isFinished) || isGranted(_.ManageTournament) =>
f(t)
case Some(t) => Redirect(routes.Tournament.show(t.id)).fuccess
case _ => notFound
}
private val streamerCache = env.memo.cacheApi[Tour.ID, Set[UserModel.ID]](64, "tournament.streamers") {
_.refreshAfterWrite(15.seconds)
2019-12-23 21:08:41 -07:00
.maximumSize(64)
2019-12-23 18:01:45 -07:00
.buildAsyncFuture { tourId =>
2019-12-23 21:08:41 -07:00
env.tournament.tournamentRepo.isUnfinished(tourId) flatMap {
_ ?? {
env.streamer.liveStreamApi.all.flatMap {
_.streams
.map { stream =>
env.tournament.hasUser(tourId, stream.streamer.userId).dmap(_ option stream.streamer.userId)
}
.sequenceFu
.dmap(_.flatten.toSet)
2019-12-23 18:01:45 -07:00
}
2019-12-23 21:08:41 -07:00
}
2019-12-23 18:01:45 -07:00
}
}
}
2019-12-08 10:35:26 -07:00
private def getUserTeamIds(user: lila.user.User): Fu[List[TeamID]] =
2019-12-04 16:39:16 -07:00
env.team.cached.teamIdsList(user.id)
2013-05-07 17:44:26 -06:00
}