2016-02-27 04:30:38 -07:00
|
|
|
package controllers
|
|
|
|
|
2016-04-18 23:57:56 -06:00
|
|
|
import play.api.libs.json._
|
2016-02-27 04:30:38 -07:00
|
|
|
import play.api.mvc._
|
|
|
|
import scala.concurrent.duration._
|
|
|
|
|
2016-10-20 08:04:26 -06:00
|
|
|
import lila.api.Context
|
2016-02-27 04:30:38 -07:00
|
|
|
import lila.app._
|
2017-09-28 15:11:25 -06:00
|
|
|
import lila.chat.Chat
|
2017-02-15 17:53:15 -07:00
|
|
|
import lila.common.{ IpAddress, HTTPRequest }
|
2017-09-28 15:11:25 -06:00
|
|
|
import lila.study.JsonView.JsData
|
2016-10-22 03:07:47 -06:00
|
|
|
import lila.study.Study.WithChapter
|
2017-01-20 06:00:42 -07:00
|
|
|
import lila.study.{ Chapter, Order, Study => StudyModel }
|
2017-09-28 15:11:25 -06:00
|
|
|
import lila.tree.Node.partitionTreeJsonWriter
|
2016-02-27 04:30:38 -07:00
|
|
|
import views._
|
|
|
|
|
|
|
|
object Study extends LilaController {
|
|
|
|
|
2016-06-21 07:02:23 -06:00
|
|
|
type ListUrl = String => Call
|
2016-05-27 05:49:17 -06:00
|
|
|
|
2016-02-27 04:30:38 -07:00
|
|
|
private def env = Env.study
|
|
|
|
|
2016-07-25 12:14:43 -06:00
|
|
|
def search(text: String, page: Int) = OpenBody { implicit ctx =>
|
2016-07-31 15:22:50 -06:00
|
|
|
Reasonable(page) {
|
|
|
|
if (text.trim.isEmpty)
|
|
|
|
env.pager.all(ctx.me, Order.default, page) map { pag =>
|
|
|
|
Ok(html.study.all(pag, Order.default))
|
|
|
|
}
|
|
|
|
else Env.studySearch(ctx.me)(text, page) map { pag =>
|
|
|
|
Ok(html.study.search(pag, text))
|
2016-07-25 12:14:43 -06:00
|
|
|
}
|
2016-07-30 03:43:19 -06:00
|
|
|
}
|
2016-07-25 12:14:43 -06:00
|
|
|
}
|
|
|
|
|
2016-06-21 07:02:23 -06:00
|
|
|
def allDefault(page: Int) = all(Order.Hot.key, page)
|
|
|
|
|
2016-06-22 03:36:32 -06:00
|
|
|
def all(o: String, page: Int) = Open { implicit ctx =>
|
2016-07-31 15:22:50 -06:00
|
|
|
Reasonable(page) {
|
|
|
|
Order(o) match {
|
|
|
|
case Order.Oldest => Redirect(routes.Study.allDefault(page)).fuccess
|
|
|
|
case order =>
|
|
|
|
env.pager.all(ctx.me, order, page) map { pag =>
|
|
|
|
Ok(html.study.all(pag, order))
|
|
|
|
}
|
|
|
|
}
|
2016-06-21 07:02:23 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-05-27 05:49:17 -06:00
|
|
|
def byOwnerDefault(username: String, page: Int) = byOwner(username, Order.default.key, page)
|
|
|
|
|
|
|
|
def byOwner(username: String, order: String, page: Int) = Open { implicit ctx =>
|
2016-05-11 10:21:03 -06:00
|
|
|
OptionFuOk(lila.user.UserRepo named username) { owner =>
|
2016-06-21 07:02:23 -06:00
|
|
|
env.pager.byOwner(owner, ctx.me, Order(order), page) map { pag =>
|
2016-05-27 05:49:17 -06:00
|
|
|
html.study.byOwner(pag, Order(order), owner)
|
2016-05-11 10:21:03 -06:00
|
|
|
}
|
2016-05-11 10:47:46 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-10-08 12:49:51 -06:00
|
|
|
def mine(order: String, page: Int) = Auth { implicit ctx => me =>
|
|
|
|
env.pager.mine(me, Order(order), page) map { pag =>
|
|
|
|
Ok(html.study.mine(pag, Order(order), me))
|
|
|
|
}
|
2016-05-11 10:47:46 -06:00
|
|
|
}
|
|
|
|
|
2016-10-08 12:49:51 -06:00
|
|
|
def minePublic(order: String, page: Int) = Auth { implicit ctx => me =>
|
|
|
|
env.pager.minePublic(me, Order(order), page) map { pag =>
|
|
|
|
Ok(html.study.minePublic(pag, Order(order), me))
|
|
|
|
}
|
2016-05-11 10:21:03 -06:00
|
|
|
}
|
|
|
|
|
2016-10-08 12:49:51 -06:00
|
|
|
def minePrivate(order: String, page: Int) = Auth { implicit ctx => me =>
|
|
|
|
env.pager.minePrivate(me, Order(order), page) map { pag =>
|
|
|
|
Ok(html.study.minePrivate(pag, Order(order), me))
|
|
|
|
}
|
2016-05-11 10:21:03 -06:00
|
|
|
}
|
|
|
|
|
2016-10-08 12:49:51 -06:00
|
|
|
def mineMember(order: String, page: Int) = Auth { implicit ctx => me =>
|
|
|
|
env.pager.mineMember(me, Order(order), page) map { pag =>
|
|
|
|
Ok(html.study.mineMember(pag, Order(order), me))
|
|
|
|
}
|
2016-06-21 07:02:23 -06:00
|
|
|
}
|
|
|
|
|
2016-10-08 12:49:51 -06:00
|
|
|
def mineLikes(order: String, page: Int) = Auth { implicit ctx => me =>
|
|
|
|
env.pager.mineLikes(me, Order(order), page) map { pag =>
|
|
|
|
Ok(html.study.mineLikes(pag, Order(order), me))
|
|
|
|
}
|
2016-05-26 17:43:13 -06:00
|
|
|
}
|
|
|
|
|
2016-10-22 03:07:47 -06:00
|
|
|
private def showQuery(query: Fu[Option[WithChapter]])(implicit ctx: Context) =
|
2017-09-28 15:11:25 -06:00
|
|
|
OptionFuResult(query) { oldSc =>
|
|
|
|
CanViewResult(oldSc.study) {
|
2017-01-26 07:28:16 -07:00
|
|
|
for {
|
2017-09-28 15:11:25 -06:00
|
|
|
(sc, data) <- getJsonData(oldSc)
|
2017-01-26 07:28:16 -07:00
|
|
|
res <- negotiate(
|
|
|
|
html = for {
|
2017-09-28 15:11:25 -06:00
|
|
|
chat <- chatOf(sc.study)
|
|
|
|
sVersion <- env.version(sc.study.id)
|
|
|
|
} yield Ok(html.study.show(sc.study, data, chat, sVersion)),
|
2017-01-26 07:28:16 -07:00
|
|
|
api = _ => Ok(Json.obj(
|
2017-08-23 17:56:39 -06:00
|
|
|
"study" -> data.study,
|
|
|
|
"analysis" -> data.analysis
|
|
|
|
)).fuccess
|
2017-01-26 07:28:16 -07:00
|
|
|
)
|
|
|
|
} yield res
|
2016-04-26 21:23:34 -06:00
|
|
|
}
|
2016-04-16 07:26:01 -06:00
|
|
|
} map NoCache
|
2016-02-27 04:30:38 -07:00
|
|
|
|
2017-09-28 15:11:25 -06:00
|
|
|
private[controllers] def getJsonData(sc: WithChapter)(implicit ctx: Context): Fu[(WithChapter, JsData)] = for {
|
|
|
|
chapters <- Env.study.chapterRepo.orderedMetadataByStudy(sc.study.id)
|
|
|
|
(study, resetToChapter) <- Env.study.api.resetIfOld(sc.study, chapters)
|
|
|
|
chapter = resetToChapter | sc.chapter
|
|
|
|
_ <- Env.user.lightUserApi preloadMany study.members.ids.toList
|
|
|
|
_ = if (HTTPRequest isSynchronousHttp ctx.req) Env.study.studyRepo.incViews(study)
|
|
|
|
initialFen = chapter.root.fen.value.some
|
|
|
|
pov = UserAnalysis.makePov(initialFen, chapter.setup.variant)
|
|
|
|
baseData = Env.round.jsonView.userAnalysisJson(pov, ctx.pref, initialFen, chapter.setup.orientation, owner = false, me = ctx.me)
|
|
|
|
studyJson <- Env.study.jsonView(study, chapters, chapter, ctx.me)
|
|
|
|
} yield WithChapter(study, chapter) -> JsData(
|
|
|
|
study = studyJson,
|
|
|
|
analysis = baseData ++ Json.obj(
|
|
|
|
"treeParts" -> partitionTreeJsonWriter.writes {
|
|
|
|
lila.study.TreeBuilder(chapter.root, chapter.setup.variant)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2016-10-22 03:07:47 -06:00
|
|
|
def show(id: String) = Open { implicit ctx =>
|
|
|
|
showQuery(env.api byIdWithChapter id)
|
|
|
|
}
|
2016-06-13 05:29:00 -06:00
|
|
|
|
2016-05-31 05:16:07 -06:00
|
|
|
def chapter(id: String, chapterId: String) = Open { implicit ctx =>
|
2016-10-22 03:22:30 -06:00
|
|
|
showQuery(env.api.byIdWithChapter(id, chapterId))
|
|
|
|
}
|
|
|
|
|
|
|
|
def chapterMeta(id: String, chapterId: String) = Open { implicit ctx =>
|
|
|
|
env.chapterRepo.byId(chapterId).map {
|
2017-01-21 06:56:51 -07:00
|
|
|
_.filter(_.studyId.value == id) ?? { chapter =>
|
2016-10-22 03:22:30 -06:00
|
|
|
Ok(env.jsonView.chapterConfig(chapter))
|
|
|
|
}
|
|
|
|
}
|
2016-05-31 05:16:07 -06:00
|
|
|
}
|
|
|
|
|
2017-09-28 15:11:25 -06:00
|
|
|
private[controllers] def chatOf(study: lila.study.Study)(implicit ctx: Context) =
|
2017-08-17 14:25:50 -06:00
|
|
|
ctx.noKid ?? Env.chat.api.userChat.findMine(Chat.Id(study.id.value), ctx.me).map(some)
|
2016-10-22 03:07:47 -06:00
|
|
|
|
2017-09-09 09:01:35 -06:00
|
|
|
def websocket(id: String, apiVersion: Int) = SocketOption[JsValue] { implicit ctx =>
|
2016-04-18 04:51:48 -06:00
|
|
|
get("sri") ?? { uid =>
|
|
|
|
env.api byId id flatMap {
|
2016-04-26 21:23:34 -06:00
|
|
|
_.filter(canView) ?? { study =>
|
2016-04-18 04:51:48 -06:00
|
|
|
env.socketHandler.join(
|
|
|
|
studyId = id,
|
2016-04-23 04:01:21 -06:00
|
|
|
uid = lila.socket.Socket.Uid(uid),
|
2017-06-16 08:27:38 -06:00
|
|
|
user = ctx.me
|
2017-02-14 08:34:07 -07:00
|
|
|
)
|
2016-04-18 04:51:48 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-29 18:05:43 -06:00
|
|
|
def createAs = AuthBody { implicit ctx => me =>
|
|
|
|
implicit val req = ctx.body
|
|
|
|
lila.study.DataForm.form.bindFromRequest.fold(
|
|
|
|
err => Redirect(routes.Study.byOwnerDefault(me.username)).fuccess,
|
|
|
|
data => for {
|
|
|
|
owner <- env.studyRepo.recentByOwner(me.id, 50)
|
|
|
|
contrib <- env.studyRepo.recentByContributor(me.id, 50)
|
|
|
|
res <- if (owner.isEmpty && contrib.isEmpty) createStudy(data, me)
|
|
|
|
else Ok(html.study.create(data, owner, contrib)).fuccess
|
|
|
|
} yield res
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2016-10-08 12:49:51 -06:00
|
|
|
def create = AuthBody { implicit ctx => me =>
|
|
|
|
implicit val req = ctx.body
|
|
|
|
lila.study.DataForm.form.bindFromRequest.fold(
|
|
|
|
err => Redirect(routes.Study.byOwnerDefault(me.username)).fuccess,
|
2017-03-29 18:05:43 -06:00
|
|
|
data => createStudy(data, me)
|
2017-02-14 08:34:07 -07:00
|
|
|
)
|
2016-02-27 04:30:38 -07:00
|
|
|
}
|
2016-04-26 01:03:49 -06:00
|
|
|
|
2017-03-29 18:05:43 -06:00
|
|
|
private def createStudy(data: lila.study.DataForm.Data, me: lila.user.User)(implicit ctx: Context) =
|
2017-09-29 12:33:37 -06:00
|
|
|
env.api.create(lila.study.StudyMaker.Data(data), me) flatMap {
|
2017-03-29 18:05:43 -06:00
|
|
|
_.fold(notFound) { sc =>
|
|
|
|
Redirect(routes.Study.show(sc.study.id.value)).fuccess
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-10-08 12:49:51 -06:00
|
|
|
def delete(id: String) = Auth { implicit ctx => me =>
|
2017-04-01 06:16:56 -06:00
|
|
|
env.api.byIdAndOwner(id, me) flatMap {
|
|
|
|
_ ?? env.api.delete
|
2017-07-16 07:05:18 -06:00
|
|
|
} inject Redirect(routes.Study.mine("hot"))
|
2016-05-12 08:21:25 -06:00
|
|
|
}
|
|
|
|
|
2017-04-01 06:16:56 -06:00
|
|
|
def clearChat(id: String) = Auth { implicit ctx => me =>
|
|
|
|
env.api.isOwner(id, me) flatMap {
|
2017-08-17 14:25:50 -06:00
|
|
|
_ ?? Env.chat.api.userChat.clear(Chat.Id(id))
|
2017-04-01 06:16:56 -06:00
|
|
|
} inject Redirect(routes.Study.show(id))
|
|
|
|
}
|
|
|
|
|
2016-10-08 12:49:51 -06:00
|
|
|
def embed(id: String, chapterId: String) = Open { implicit ctx =>
|
2016-10-20 08:04:26 -06:00
|
|
|
env.api.byIdWithChapter(id, chapterId) flatMap {
|
|
|
|
_.fold(embedNotFound) {
|
2017-01-20 05:47:52 -07:00
|
|
|
case WithChapter(study, chapter) => CanViewResult(study) {
|
2017-05-02 06:36:46 -06:00
|
|
|
env.jsonView(study.copy(
|
|
|
|
members = lila.study.StudyMembers(Map.empty) // don't need no members
|
|
|
|
), List(chapter.metadata), chapter, ctx.me) flatMap { studyJson =>
|
|
|
|
val setup = chapter.setup
|
|
|
|
val initialFen = chapter.root.fen.value.some
|
|
|
|
val pov = UserAnalysis.makePov(initialFen, setup.variant)
|
|
|
|
val baseData = Env.round.jsonView.userAnalysisJson(pov, ctx.pref, initialFen, setup.orientation, owner = false, me = ctx.me)
|
|
|
|
val analysis = baseData ++ Json.obj(
|
|
|
|
"treeParts" -> partitionTreeJsonWriter.writes {
|
|
|
|
lila.study.TreeBuilder.makeRoot(chapter.root)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
val data = lila.study.JsonView.JsData(
|
|
|
|
study = studyJson,
|
|
|
|
analysis = analysis
|
|
|
|
)
|
|
|
|
negotiate(
|
|
|
|
html = Ok(html.study.embed(study, chapter, data)).fuccess,
|
|
|
|
api = _ => Ok(Json.obj("study" -> data.study, "analysis" -> data.analysis)).fuccess
|
|
|
|
)
|
|
|
|
}
|
2016-10-20 08:04:26 -06:00
|
|
|
}
|
2016-08-31 10:00:45 -06:00
|
|
|
}
|
2016-10-08 12:49:51 -06:00
|
|
|
} map NoCache
|
|
|
|
}
|
|
|
|
|
2016-10-20 08:04:26 -06:00
|
|
|
private def embedNotFound(implicit ctx: Context): Fu[Result] =
|
|
|
|
fuccess(NotFound(html.study.embedNotFound()))
|
|
|
|
|
2016-10-08 12:49:51 -06:00
|
|
|
def cloneStudy(id: String) = Auth { implicit ctx => me =>
|
|
|
|
OptionFuResult(env.api.byId(id)) { study =>
|
|
|
|
CanViewResult(study) {
|
|
|
|
Ok(html.study.clone(study)).fuccess
|
|
|
|
}
|
|
|
|
}
|
2016-08-31 10:00:45 -06:00
|
|
|
}
|
|
|
|
|
2017-02-15 17:53:15 -07:00
|
|
|
private val CloneLimitPerUser = new lila.memo.RateLimit[lila.user.User.ID](
|
2016-10-31 16:39:42 -06:00
|
|
|
credits = 10 * 3,
|
2016-08-31 10:00:45 -06:00
|
|
|
duration = 24 hour,
|
2016-09-01 15:54:43 -06:00
|
|
|
name = "clone study per user",
|
2017-02-14 08:34:07 -07:00
|
|
|
key = "clone_study.user"
|
|
|
|
)
|
2016-08-31 10:00:45 -06:00
|
|
|
|
2017-02-15 17:53:15 -07:00
|
|
|
private val CloneLimitPerIP = new lila.memo.RateLimit[IpAddress](
|
2016-10-31 16:39:42 -06:00
|
|
|
credits = 20 * 3,
|
2016-08-31 10:00:45 -06:00
|
|
|
duration = 24 hour,
|
2016-09-01 15:54:43 -06:00
|
|
|
name = "clone study per IP",
|
2017-02-14 08:34:07 -07:00
|
|
|
key = "clone_study.ip"
|
|
|
|
)
|
2016-08-31 10:00:45 -06:00
|
|
|
|
2016-10-08 12:49:51 -06:00
|
|
|
def cloneApply(id: String) = Auth { implicit ctx => me =>
|
|
|
|
implicit val default = ornicar.scalalib.Zero.instance[Fu[Result]](notFound)
|
2016-10-31 16:39:42 -06:00
|
|
|
val cost = if (isGranted(_.Coach) || me.hasTitle) 1 else 3
|
|
|
|
CloneLimitPerUser(me.id, cost = cost) {
|
|
|
|
CloneLimitPerIP(HTTPRequest lastRemoteAddress ctx.req, cost = cost) {
|
2016-10-08 12:49:51 -06:00
|
|
|
OptionFuResult(env.api.byId(id)) { prev =>
|
|
|
|
CanViewResult(prev) {
|
|
|
|
env.api.clone(me, prev) map { study =>
|
2017-01-20 05:47:52 -07:00
|
|
|
Redirect(routes.Study.show((study | prev).id.value))
|
2016-08-31 10:00:45 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2016-10-08 12:49:51 -06:00
|
|
|
}
|
2016-08-31 10:00:45 -06:00
|
|
|
}
|
|
|
|
|
2017-02-15 17:53:15 -07:00
|
|
|
private val PgnRateLimitGlobal = new lila.memo.RateLimit[String](
|
2016-08-18 03:16:54 -06:00
|
|
|
credits = 30,
|
|
|
|
duration = 1 minute,
|
2016-09-01 15:54:43 -06:00
|
|
|
name = "export study PGN global",
|
2017-02-14 08:34:07 -07:00
|
|
|
key = "export.study_pgn.global"
|
|
|
|
)
|
2016-08-18 03:16:54 -06:00
|
|
|
|
2016-04-26 01:03:49 -06:00
|
|
|
def pgn(id: String) = Open { implicit ctx =>
|
2016-08-18 03:16:54 -06:00
|
|
|
OnlyHumans {
|
2017-02-15 17:53:15 -07:00
|
|
|
PgnRateLimitGlobal("-", msg = HTTPRequest.lastRemoteAddress(ctx.req).value) {
|
2016-08-18 03:16:54 -06:00
|
|
|
OptionFuResult(env.api byId id) { study =>
|
|
|
|
CanViewResult(study) {
|
|
|
|
lila.mon.export.pgn.study()
|
|
|
|
env.pgnDump(study) map { pgns =>
|
|
|
|
Ok(pgns.mkString("\n\n\n")).withHeaders(
|
2017-09-09 09:01:35 -06:00
|
|
|
CONTENT_TYPE -> pgnContentType,
|
2017-02-14 08:34:07 -07:00
|
|
|
CONTENT_DISPOSITION -> ("attachment; filename=" + (env.pgnDump filename study))
|
2017-09-09 09:01:35 -06:00
|
|
|
)
|
2016-08-18 03:16:54 -06:00
|
|
|
}
|
|
|
|
}
|
2016-04-26 21:23:34 -06:00
|
|
|
}
|
2016-04-26 01:03:49 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2016-04-26 21:23:34 -06:00
|
|
|
|
2016-10-21 07:02:57 -06:00
|
|
|
def chapterPgn(id: String, chapterId: String) = Open { implicit ctx =>
|
|
|
|
OnlyHumans {
|
|
|
|
env.api.byIdWithChapter(id, chapterId) flatMap {
|
|
|
|
_.fold(notFound) {
|
2017-01-20 05:47:52 -07:00
|
|
|
case WithChapter(study, chapter) => CanViewResult(study) {
|
2016-10-21 07:02:57 -06:00
|
|
|
lila.mon.export.pgn.studyChapter()
|
2016-12-21 06:44:18 -07:00
|
|
|
Ok(env.pgnDump.ofChapter(study, chapter).toString).withHeaders(
|
2017-09-09 09:01:35 -06:00
|
|
|
CONTENT_TYPE -> pgnContentType,
|
2016-12-21 06:44:18 -07:00
|
|
|
CONTENT_DISPOSITION -> ("attachment; filename=" + (env.pgnDump.filename(study, chapter)))
|
2017-09-09 09:01:35 -06:00
|
|
|
).fuccess
|
2016-10-21 07:02:57 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-01-20 05:47:52 -07:00
|
|
|
private def CanViewResult(study: StudyModel)(f: => Fu[Result])(implicit ctx: lila.api.Context) =
|
2016-04-26 21:23:34 -06:00
|
|
|
if (canView(study)) f
|
2017-06-13 03:22:57 -06:00
|
|
|
else negotiate(
|
|
|
|
html = fuccess(Unauthorized(html.study.restricted(study))),
|
|
|
|
api = _ => fuccess(Unauthorized(jsonError("This study is now private")))
|
|
|
|
)
|
2016-04-26 21:23:34 -06:00
|
|
|
|
2017-01-20 05:47:52 -07:00
|
|
|
private def canView(study: StudyModel)(implicit ctx: lila.api.Context) =
|
2017-08-16 22:35:45 -06:00
|
|
|
!study.isPrivate || ctx.userId.exists(study.members.contains)
|
2017-01-20 05:47:52 -07:00
|
|
|
|
|
|
|
private implicit def makeStudyId(id: String): StudyModel.Id = StudyModel.Id(id)
|
2017-01-20 06:00:42 -07:00
|
|
|
private implicit def makeChapterId(id: String): Chapter.Id = Chapter.Id(id)
|
2016-02-27 04:30:38 -07:00
|
|
|
}
|