lila/app/controllers/Team.scala

540 lines
17 KiB
Scala
Raw Normal View History

package controllers
2020-04-12 14:30:39 -06:00
import play.api.data.Form
2020-07-10 09:06:46 -06:00
import play.api.data.Forms._
2019-10-02 10:50:09 -06:00
import play.api.libs.json._
import play.api.mvc._
2020-08-12 00:53:51 -06:00
import scala.concurrent.duration._
2020-08-02 09:48:52 -06:00
import views._
2019-10-02 10:50:09 -06:00
2015-04-12 00:59:13 -06:00
import lila.api.Context
import lila.app._
2019-12-04 23:52:53 -07:00
import lila.common.config.MaxPerSecond
2021-01-14 10:01:51 -07:00
import lila.team.{ Requesting, Team => TeamModel }
2014-02-17 02:12:19 -07:00
import lila.user.{ User => UserModel }
2019-12-04 23:52:53 -07:00
final class Team(
env: Env,
2019-12-05 14:51:18 -07:00
apiC: => Api
2019-12-04 23:52:53 -07:00
) extends LilaController(env) {
2019-12-13 07:30:20 -07:00
private def forms = env.team.forms
private def api = env.team.api
2019-12-04 16:39:16 -07:00
private def paginator = env.team.paginator
2013-04-09 12:58:34 -06:00
2020-05-05 22:11:15 -06:00
def all(page: Int) =
Open { implicit ctx =>
2020-11-03 00:24:34 -07:00
Reasonable(page) {
paginator popularTeams page map {
html.team.list.all(_)
}
2020-05-05 22:11:15 -06:00
}
2020-04-24 10:10:18 -06:00
}
2013-05-06 14:49:12 -06:00
2020-05-05 22:11:15 -06:00
def home(page: Int) =
Open { implicit ctx =>
ctx.me.??(api.hasTeams) map {
2020-07-07 02:34:48 -06:00
case true => Redirect(routes.Team.mine())
2020-05-05 22:11:15 -06:00
case false => Redirect(routes.Team.all(page))
}
2015-09-09 09:32:49 -06:00
}
2020-05-05 22:11:15 -06:00
def show(id: String, page: Int) =
Open { implicit ctx =>
2020-11-03 00:24:34 -07:00
Reasonable(page) {
OptionFuOk(api team id) { renderTeam(_, page) }
}
2020-05-05 22:11:15 -06:00
}
2013-05-06 14:49:12 -06:00
2020-05-05 22:11:15 -06:00
def search(text: String, page: Int) =
OpenBody { implicit ctx =>
2020-11-03 00:24:34 -07:00
Reasonable(page) {
if (text.trim.isEmpty) paginator popularTeams page map { html.team.list.all(_) }
else
env.teamSearch(text, page) map { html.team.list.search(text, _) }
}
2020-05-05 22:11:15 -06:00
}
2013-05-06 14:49:12 -06:00
2019-12-13 07:30:20 -07:00
private def renderTeam(team: TeamModel, page: Int = 1)(implicit ctx: Context) =
for {
info <- env.teamInfo(team, ctx.me)
members <- paginator.teamMembers(team, page)
2020-04-26 00:41:35 -06:00
hasChat = canHaveChat(team, info)
2020-05-05 22:11:15 -06:00
chat <-
hasChat ?? env.chat.api.userChat.cached
.findMine(lila.chat.Chat.Id(team.id), ctx.me)
.map(some)
2020-04-25 18:52:13 -06:00
_ <- env.user.lightUserApi preloadMany {
info.userIds ::: chat.??(_.chat.userIds)
}
2020-04-26 00:41:35 -06:00
version <- hasChat ?? env.team.version(team.id).dmap(some)
2020-04-25 18:52:13 -06:00
} yield html.team.show(team, members, info, chat, version)
2020-04-26 00:41:35 -06:00
private def canHaveChat(team: TeamModel, info: lila.app.mashup.TeamInfo)(implicit ctx: Context): Boolean =
2020-08-12 02:23:06 -06:00
team.enabled && !team.isChatFor(_.NONE) && ctx.noKid && {
2020-04-26 11:38:03 -06:00
(team.isChatFor(_.LEADERS) && ctx.userId.exists(team.leaders)) ||
(team.isChatFor(_.MEMBERS) && info.mine) ||
isGranted(_.ChatTimeout)
2020-04-26 11:16:35 -06:00
}
2013-05-06 14:49:12 -06:00
2020-05-05 22:11:15 -06:00
def users(teamId: String) =
Action.async { implicit req =>
api.team(teamId) flatMap {
_ ?? { team =>
apiC.jsonStream {
env.team
.memberStream(team, MaxPerSecond(20))
.map(env.api.userApi.one)
}.fuccess
}
}
}
2020-05-05 22:11:15 -06:00
def tournaments(teamId: String) =
Open { implicit ctx =>
env.team.teamRepo.enabled(teamId) flatMap {
_ ?? { team =>
env.teamInfo.tournaments(team, 30, 30) map { tours =>
2020-05-06 14:56:17 -06:00
Ok(html.team.tournaments.page(team, tours))
2020-05-05 22:11:15 -06:00
}
2020-04-26 12:25:40 -06:00
}
}
}
2020-05-05 22:11:15 -06:00
def edit(id: String) =
Auth { implicit ctx => _ =>
WithOwnedTeam(id) { team =>
fuccess(html.team.form.edit(team, forms edit team))
}
2017-01-26 04:05:21 -07:00
}
2020-05-05 22:11:15 -06:00
def update(id: String) =
AuthBody { implicit ctx => me =>
WithOwnedTeam(id) { team =>
implicit val req = ctx.body
forms
.edit(team)
2020-07-07 02:34:48 -06:00
.bindFromRequest()
2020-05-05 22:11:15 -06:00
.fold(
err => BadRequest(html.team.form.edit(team, err)).fuccess,
data => api.update(team, data, me) inject Redirect(routes.Team.show(team.id)).flashSuccess
)
}
2013-05-06 14:49:12 -06:00
}
2020-05-05 22:11:15 -06:00
def kickForm(id: String) =
Auth { implicit ctx => me =>
WithOwnedTeam(id) { team =>
env.team.memberRepo userIdsByTeam team.id map { userIds =>
html.team.admin.kick(team, userIds - me.id)
}
2013-05-06 14:49:12 -06:00
}
}
2020-05-05 22:11:15 -06:00
def kick(id: String) =
AuthBody { implicit ctx => me =>
WithOwnedTeam(id) { team =>
implicit val req = ctx.body
2020-07-07 02:34:48 -06:00
forms.selectMember.bindFromRequest().value ?? { api.kick(team, _, me) } inject Redirect(
2020-05-05 22:11:15 -06:00
routes.Team.show(team.id)
).flashSuccess
}
2013-05-06 14:49:12 -06:00
}
2020-05-05 22:11:15 -06:00
def kickUser(teamId: String, userId: String) =
Scoped(_.Team.Write) { _ => me =>
api team teamId flatMap {
_ ?? { team =>
if (team leaders me.id) api.kick(team, userId, me) inject jsonOkResult
else Forbidden(jsonError("Not your team")).fuccess
}
2019-07-23 06:39:25 -06:00
}
}
2013-05-06 14:49:12 -06:00
2020-05-05 22:11:15 -06:00
def leadersForm(id: String) =
Auth { implicit ctx => _ =>
WithOwnedTeam(id) { team =>
Ok(html.team.admin.leaders(team, forms leaders team)).fuccess
}
}
2020-05-05 22:11:15 -06:00
def leaders(id: String) =
AuthBody { implicit ctx => me =>
2020-05-05 22:11:15 -06:00
WithOwnedTeam(id) { team =>
implicit val req = ctx.body
2020-08-02 09:48:52 -06:00
forms.leaders(team).bindFromRequest().value ?? {
api.setLeaders(team, _, me, isGranted(_.ManageTeam))
} inject Redirect(
2020-05-05 22:11:15 -06:00
routes.Team.show(team.id)
).flashSuccess
}
}
2020-05-05 22:11:15 -06:00
def close(id: String) =
Secure(_.ManageTeam) { implicit ctx => me =>
OptionFuResult(api team id) { team =>
2020-08-20 13:23:44 -06:00
api.delete(team) >>
2020-08-24 05:05:59 -06:00
env.mod.logApi.deleteTeam(me.id, team.id, team.name) inject
2020-05-05 22:11:15 -06:00
Redirect(routes.Team all 1).flashSuccess
}
2017-01-15 05:26:08 -07:00
}
2020-07-23 04:33:04 -06:00
def disable(id: String) =
Auth { implicit ctx => me =>
WithOwnedTeam(id) { team =>
2020-08-20 13:23:44 -06:00
api.disable(team, me) >>
2020-08-24 05:05:59 -06:00
env.mod.logApi.disableTeam(me.id, team.id, team.name) inject
2020-07-23 04:33:04 -06:00
Redirect(routes.Team show id).flashSuccess
}
}
2020-05-05 22:11:15 -06:00
def form =
Auth { implicit ctx => me =>
2020-05-08 09:00:28 -06:00
LimitPerWeek(me) {
2020-05-05 22:11:15 -06:00
forms.anyCaptcha map { captcha =>
Ok(html.team.form.create(forms.create, captcha))
}
2013-05-06 14:49:12 -06:00
}
2017-01-15 05:26:08 -07:00
}
2020-05-05 22:11:15 -06:00
def create =
AuthBody { implicit ctx => implicit me =>
2020-05-08 09:00:28 -06:00
LimitPerWeek(me) {
2020-05-05 22:11:15 -06:00
implicit val req = ctx.body
2020-07-10 09:06:46 -06:00
forms.create
.bindFromRequest()
.fold(
err =>
forms.anyCaptcha map { captcha =>
BadRequest(html.team.form.create(err, captcha))
},
data =>
api.create(data, me) map { team =>
Redirect(routes.Team.show(team.id)).flashSuccess
}
)
2020-05-05 22:11:15 -06:00
}
2017-01-26 04:05:21 -07:00
}
2013-05-06 14:49:12 -06:00
2020-05-05 22:11:15 -06:00
def mine =
Auth { implicit ctx => me =>
api mine me map {
html.team.list.mine(_)
}
2020-04-24 10:10:18 -06:00
}
2013-05-06 14:49:12 -06:00
2020-05-05 22:11:15 -06:00
def leader =
Auth { implicit ctx => me =>
env.team.teamRepo enabledTeamsByLeader me.id map {
html.team.list.ledByMe(_)
}
}
2013-05-06 14:49:12 -06:00
2020-05-05 22:11:15 -06:00
def join(id: String) =
AuthOrScopedBody(_.Team.Write)(
2020-05-05 22:11:15 -06:00
auth = implicit ctx =>
me =>
2021-01-13 12:42:12 -07:00
api.team(id) flatMap {
_ ?? { team =>
api countTeamsOf me flatMap { nb =>
if (nb >= TeamModel.maxJoin)
negotiate(
html = BadRequest(views.html.site.message.teamJoinLimit).fuccess,
api = _ => BadRequest(jsonError("You have joined too many teams")).fuccess
)
else
negotiate(
2021-01-14 10:01:51 -07:00
html = webJoin(team, me, request = none, password = none),
2021-01-13 12:42:12 -07:00
api = _ => {
implicit val body = ctx.body
forms
.apiRequest(team)
.bindFromRequest()
.fold(
newJsonFormError,
setup =>
2021-01-14 10:01:51 -07:00
api.join(team, me, setup.message, setup.password) flatMap {
case Requesting.Joined => jsonOkResult.fuccess
case Requesting.NeedRequest =>
2021-01-13 12:42:12 -07:00
BadRequest(jsonError("This team requires confirmation.")).fuccess
2021-01-14 10:01:51 -07:00
case Requesting.NeedPassword =>
BadRequest(jsonError("This team requires a password.")).fuccess
2021-01-13 12:42:12 -07:00
case _ => notFoundJson("Team not found")
}
)
}
)
}
}
2020-05-05 22:11:15 -06:00
},
scoped = implicit req =>
2021-01-13 12:42:12 -07:00
me =>
api.team(id) flatMap {
_ ?? { team =>
implicit val lang = reqLang
forms
.apiRequest(team)
.bindFromRequest()
.fold(
newJsonFormError,
setup =>
env.oAuth.server.fetchAppAuthor(req) flatMap {
2021-01-14 10:01:51 -07:00
api.joinApi(team, me, _, setup.message)
2021-01-13 12:42:12 -07:00
} flatMap {
2021-01-14 10:01:51 -07:00
case Requesting.Joined => jsonOkResult.fuccess
case Requesting.NeedRequest =>
2021-01-13 12:42:12 -07:00
Forbidden(
jsonError(
"This team requires confirmation, and is not owned by the oAuth app owner."
)
).fuccess
}
)
}
}
2020-05-05 22:11:15 -06:00
)
2013-05-06 14:49:12 -06:00
2020-07-10 09:06:46 -06:00
def subscribe(teamId: String) = {
def doSub(req: Request[_], me: UserModel) =
2020-09-06 09:16:05 -06:00
Form(single("subscribe" -> optional(boolean)))
2020-07-10 09:06:46 -06:00
.bindFromRequest()(req)
2020-09-06 09:16:05 -06:00
.fold(_ => funit, v => api.subscribe(teamId, me.id, ~v))
2020-07-10 09:06:46 -06:00
AuthOrScopedBody(_.Team.Write)(
2020-09-06 09:16:05 -06:00
auth = ctx => me => doSub(ctx.body, me) inject jsonOkResult,
2020-07-10 09:06:46 -06:00
scoped = req => me => doSub(req, me) inject jsonOkResult
)
}
2020-05-05 22:11:15 -06:00
def requests =
Auth { implicit ctx => me =>
import lila.memo.CacheApi._
env.team.cached.nbRequests invalidate me.id
api requestsWithUsers me map { html.team.request.all(_) }
2017-01-26 04:05:21 -07:00
}
2020-05-05 22:11:15 -06:00
def requestForm(id: String) =
Auth { implicit ctx => me =>
OptionFuOk(api.requestable(id, me)) { team =>
2020-12-14 11:21:34 -07:00
fuccess(html.team.request.requestForm(team, forms.request(team)))
2020-05-05 22:11:15 -06:00
}
2013-05-06 14:49:12 -06:00
}
2020-05-05 22:11:15 -06:00
def requestCreate(id: String) =
AuthBody { implicit ctx => me =>
OptionFuResult(api.requestable(id, me)) { team =>
2017-01-26 04:05:21 -07:00
implicit val req = ctx.body
2020-12-14 11:21:34 -07:00
forms
.request(team)
2020-07-10 09:06:46 -06:00
.bindFromRequest()
.fold(
2020-12-13 08:17:35 -07:00
err => BadRequest(html.team.request.requestForm(team, err)).fuccess,
2020-07-10 09:06:46 -06:00
setup =>
2021-01-14 10:01:51 -07:00
if (team.open) webJoin(team, me, request = none, password = setup.password)
else
setup.message ?? { msg =>
api.createRequest(team, me, msg) inject Redirect(routes.Team.show(team.id)).flashSuccess
}
2020-07-10 09:06:46 -06:00
)
2020-05-05 22:11:15 -06:00
}
2017-01-26 04:05:21 -07:00
}
2020-05-05 22:11:15 -06:00
2021-01-14 10:01:51 -07:00
private def webJoin(team: TeamModel, me: UserModel, request: Option[String], password: Option[String]) =
api.join(team, me, request = request, password = password) flatMap {
case Requesting.Joined => Redirect(routes.Team.show(team.id)).flashSuccess.fuccess
case Requesting.NeedRequest | Requesting.NeedPassword =>
Redirect(routes.Team.requestForm(team.id)).flashSuccess.fuccess
}
2020-05-05 22:11:15 -06:00
def requestProcess(requestId: String) =
AuthBody { implicit ctx => me =>
2020-08-12 00:53:51 -06:00
import cats.implicits._
2020-05-05 22:11:15 -06:00
OptionFuRedirectUrl(for {
requestOption <- api request requestId
teamOption <- requestOption.??(req => env.team.teamRepo.byLeader(req.team, me.id))
2020-09-21 01:28:28 -06:00
} yield (teamOption, requestOption).mapN((_, _))) { case (team, request) =>
implicit val req = ctx.body
forms.processRequest
.bindFromRequest()
.fold(
_ => fuccess(routes.Team.show(team.id).toString),
{ case (decision, url) =>
api.processRequest(team, request, decision == "accept") inject url
}
)
2020-05-05 22:11:15 -06:00
}
2019-10-02 10:50:09 -06:00
}
2020-05-05 22:11:15 -06:00
def quit(id: String) =
AuthOrScoped(_.Team.Write)(
2020-06-01 20:28:28 -06:00
auth = implicit ctx =>
2020-05-05 22:11:15 -06:00
me =>
2020-06-01 20:28:28 -06:00
OptionFuResult(api.quit(id, me)) { team =>
negotiate(
html = Redirect(routes.Team.show(team.id)).flashSuccess.fuccess,
api = _ => jsonOkResult.fuccess
)
2020-05-05 22:11:15 -06:00
}(ctx),
scoped = _ =>
me =>
api.quit(id, me) flatMap {
_.fold(notFoundJson())(_ => jsonOkResult.fuccess)
}
)
def autocomplete =
Action.async { req =>
get("term", req).filter(_.nonEmpty) match {
case None => BadRequest("No search term provided").fuccess
case Some(term) =>
for {
teams <- api.autocomplete(term, 10)
_ <- env.user.lightUserApi preloadMany teams.map(_.createdBy)
} yield Ok {
JsArray(teams map { team =>
Json.obj(
"id" -> team.id,
"name" -> team.name,
"owner" -> env.user.lightUserApi.sync(team.createdBy).fold(team.createdBy)(_.name),
"members" -> team.nbMembers
)
})
} as JSON
}
}
2020-05-05 22:11:15 -06:00
def pmAll(id: String) =
Auth { implicit ctx => _ =>
WithOwnedTeam(id) { team =>
env.tournament.api
.visibleByTeam(team.id, 0, 20)
.dmap(_.next)
2020-05-05 22:11:15 -06:00
.map { tours =>
Ok(html.team.admin.pmAll(team, forms.pmAll, tours))
}
}
}
def pmAllSubmit(id: String) =
AuthOrScopedBody(_.Team.Write)(
auth = implicit ctx =>
me =>
WithOwnedTeam(id) { team =>
doPmAll(team, me)(ctx.body).fold(
err =>
env.tournament.api
.visibleByTeam(team.id, 0, 20)
.dmap(_.next)
2020-05-05 22:11:15 -06:00
.map { tours =>
BadRequest(html.team.admin.pmAll(team, err, tours))
},
done => done inject Redirect(routes.Team.show(team.id)).flashSuccess
2020-04-06 09:44:24 -06:00
)
2020-05-05 22:11:15 -06:00
},
scoped = implicit req =>
me =>
api team id flatMap {
_.filter(_ leaders me.id) ?? { team =>
doPmAll(team, me).fold(
err => BadRequest(errorsAsJson(err)(reqLang)).fuccess,
done => done inject jsonOkResult
)
}
2020-04-06 09:44:24 -06:00
}
2020-05-05 22:11:15 -06:00
)
2020-04-06 09:44:24 -06:00
2020-04-12 11:31:33 -06:00
// API
2020-05-05 22:11:15 -06:00
def apiAll(page: Int) =
Action.async {
import env.team.jsonView._
import lila.common.paginator.PaginatorJson._
2021-01-21 04:09:02 -07:00
JsonOk {
2020-05-05 22:11:15 -06:00
paginator popularTeams page flatMap { pager =>
env.user.lightUserApi.preloadMany(pager.currentPageResults.flatMap(_.leaders)) inject pager
}
2020-04-24 10:10:18 -06:00
}
}
2020-05-05 22:11:15 -06:00
def apiShow(id: String) =
2020-05-21 20:49:31 -06:00
Open { ctx =>
JsonOptionOk {
api team id flatMap {
_ ?? { team =>
2020-05-29 12:18:44 -06:00
for {
joined <- ctx.userId.?? { api.belongsTo(id, _) }
requested <- ctx.userId.ifFalse(joined).?? { env.team.requestRepo.exists(id, _) }
} yield {
env.team.jsonView.teamWrites.writes(team) ++ Json
.obj(
"joined" -> joined,
"requested" -> requested
)
}.some
2020-05-21 20:49:31 -06:00
}
}
}
2020-04-12 11:42:31 -06:00
}
2020-04-12 11:31:33 -06:00
2020-05-05 22:11:15 -06:00
def apiSearch(text: String, page: Int) =
Action.async {
import env.team.jsonView._
import lila.common.paginator.PaginatorJson._
2021-01-21 04:09:02 -07:00
JsonOk {
2020-05-05 22:11:15 -06:00
if (text.trim.isEmpty) paginator popularTeams page
else env.teamSearch(text, page)
}
}
def apiTeamsOf(username: String) =
Action.async {
import env.team.jsonView._
2021-01-21 04:09:02 -07:00
JsonOk {
2020-05-05 22:11:15 -06:00
api teamsOf username flatMap { teams =>
env.user.lightUserApi.preloadMany(teams.flatMap(_.leaders)) inject teams
}
2020-04-24 10:19:32 -06:00
}
}
2020-04-06 09:44:24 -06:00
private def doPmAll(team: TeamModel, me: UserModel)(implicit req: Request[_]): Either[Form[_], Funit] =
2020-07-10 09:06:46 -06:00
forms.pmAll
.bindFromRequest()
2020-04-06 09:44:24 -06:00
.fold(
err => Left(err),
msg =>
2020-04-06 09:44:24 -06:00
Right {
PmAllLimitPerUser(me.id) {
2020-07-10 09:06:46 -06:00
val url = s"${env.net.baseUrl}${routes.Team.show(team.id)}"
2020-04-06 09:44:24 -06:00
val full = s"""$msg
---
2020-07-10 09:06:46 -06:00
You received this because you are subscribed to messages of the team $url."""
env.msg.api.multiPost(me, env.team.memberStream.subscribedIds(team, MaxPerSecond(50)), full)
2020-04-24 19:49:50 -06:00
funit // we don't wait for the stream to complete, it would make lichess time out
}(funit)
}
)
2020-04-24 19:49:50 -06:00
private val PmAllLimitPerUser = lila.memo.RateLimit.composite[lila.user.User.ID](
2020-07-08 11:56:35 -06:00
key = "team.pm.all",
2020-04-24 19:49:50 -06:00
enforce = env.net.rateLimit.value
)(
2020-04-29 08:58:36 -06:00
("fast", 1, 3.minutes),
("slow", 6, 24.hours)
)
2020-05-08 07:48:18 -06:00
private def LimitPerWeek[A <: Result](me: UserModel)(a: => Fu[A])(implicit ctx: Context): Fu[Result] =
api.countCreatedRecently(me) flatMap { count =>
val allow =
isGranted(_.ManageTeam) ||
(isGranted(_.Verified) && count < 100) ||
(isGranted(_.Teacher) && count < 10) ||
count < 3
if (allow) a
else Forbidden(views.html.site.message.teamCreateLimit).fuccess
2013-05-06 14:49:12 -06:00
}
private def WithOwnedTeam(teamId: String)(f: TeamModel => Fu[Result])(implicit ctx: Context): Fu[Result] =
OptionFuResult(api team teamId) { team =>
if (ctx.userId.exists(team.leaders.contains) || isGranted(_.ManageTeam)) f(team)
else renderTeam(team) map { Forbidden(_) }
}
}