appeal WIP

pull/7068/head
Thibault Duplessis 2020-07-30 10:44:05 +02:00
parent c52a7d5f78
commit 8929e375ae
10 changed files with 133 additions and 26 deletions

View File

@ -91,10 +91,9 @@ final class Env(
def net = common.netConfig
val isProd = mode == Mode.Prod
val isProdReally = isProd && net.isProd
val isProd = mode == Mode.Prod && net.isProd
val isDev = mode == Mode.Dev
val isStage = config.get[Boolean]("app.stage")
val isStage = mode == Mode.Prod && !net.isProd
val explorerEndpoint = config.get[String]("explorer.endpoint")
val tablebaseEndpoint = config.get[String]("explorer.tablebase.endpoint")
@ -121,7 +120,8 @@ final class Env(
lazy val prizeTournamentMakers = memo.settingStore[UserIds](
"prizeTournamentMakers ",
default = UserIds(Nil),
text = "User IDs who can make prize tournaments (arena & swiss) without a warning. Separated by commas.".some
text =
"User IDs who can make prize tournaments (arena & swiss) without a warning. Separated by commas.".some
)
lazy val preloader = wire[mashup.Preload]

View File

@ -1,7 +1,10 @@
package controllers
import play.api.mvc.Result
import lila.app._
import lila.api.Context
import views._
import lila.report.Suspect
final class Appeal(env: Env) extends LilaController(env) {
@ -22,8 +25,59 @@ final class Appeal(env: Env) extends LilaController(env) {
env.appeal.api.mine(me) map { appeal =>
BadRequest(html.appeal2.home(appeal, err))
},
text =>
env.appeal.api.post(text, me) inject Redirect(routes.Appeal.home()).flashSuccess
text => env.appeal.api.post(text, me) inject Redirect(routes.Appeal.home()).flashSuccess
)
}
def show(username: String) =
Secure(_.Appeals) { implicit ctx => me =>
asMod(username) { (appeal, suspect) =>
Ok(html.appeal2.show(appeal, suspect, env.appeal.forms.text)).fuccess
}
}
def reply(username: String) =
SecureBody(_.Appeals) { implicit ctx => me =>
asMod(username) { (appeal, suspect) =>
implicit val req = ctx.body
env.appeal.forms.text
.bindFromRequest()
.fold(
err => BadRequest(html.appeal2.show(appeal, suspect, err)).fuccess,
text =>
for {
_ <- env.appeal.api.reply(text, appeal, me)
_ <- env.security.automaticEmail.onAppealReply(suspect.user)
} yield Redirect(routes.Appeal.show(username)).flashSuccess
)
}
}
def act(username: String, action: String) =
Secure(_.Appeals) { implicit ctx => me =>
asMod(username) { (appeal, suspect) =>
val res = action match {
case "close" => env.appeal.api.close(appeal)
case "mute" => env.appeal.api.mute(appeal)
case "open" => env.appeal.api.open(appeal)
case _ => funit
}
res inject Redirect(routes.Appeal.show(username)).flashSuccess
}
}
private def asMod(
username: String
)(f: (lila.appeal.Appeal, Suspect) => Fu[Result])(implicit ctx: Context): Fu[Result] =
env.user.repo named username flatMap {
_ ?? { user =>
env.appeal.api get user flatMap {
_ ?? { appeal =>
f(appeal, Suspect(user)) dmap some
}
}
}
} flatMap {
_.fold(notFound)(fuccess)
}
}

View File

@ -549,7 +549,7 @@ abstract private[controllers] class LilaController(val env: Env)
type RestoredUser = (Option[FingerPrintedUser], Option[UserModel])
private def restoreUser(req: RequestHeader): Fu[RestoredUser] =
env.security.api restoreUser req dmap {
case Some(d) if !env.isProdReally =>
case Some(d) if !env.isProd =>
d.copy(user =
d.user
.addRole(lila.security.Permission.Beta.dbKey)

View File

@ -7,22 +7,17 @@ import lila.common.String.html.richText
import play.api.data.Form
import lila.appeal.Appeal
import controllers.routes
import lila.report.Suspect
import lila.user.User
import play.api.i18n.Lang
object appeal2 {
def home(appeal: Option[Appeal], textForm: Form[_])(implicit ctx: Context) =
views.html.base.layout(
moreCss = frag(
cssTag("form3"),
cssTag("appeal")
),
title = "Appeal"
) {
layout("Appeal") {
main(cls := "page-small box box-pad page appeal")(
appeal match {
case Some(a) => myAppeal(a, textForm)
case Some(a) => renderAppeal(a, textForm)
case None => newAppeal(textForm)
}
)
@ -33,11 +28,30 @@ object appeal2 {
h1("Appeal a moderation decision"),
renderHelp,
div(cls := "body")(
renderForm(textForm)
renderForm(textForm, isNew = true)
)
)
def myAppeal(appeal: Appeal, textForm: Form[_])(implicit ctx: Context) =
def show(appeal: Appeal, suspect: Suspect, textForm: Form[_])(implicit ctx: Context) =
layout(s"Appeal by ${suspect.user.username}") {
main(cls := "page-small box box-pad page appeal")(
renderAppeal(appeal, textForm),
postForm(action := routes.Appeal.close(suspect.user.username))(
submitButton("Close")
)
)
}
private def layout(title: String)(body: Frag)(implicit ctx: Context) =
views.html.base.layout(
moreCss = frag(
cssTag("form3"),
cssTag("appeal")
),
title = title
)(body)
private def renderAppeal(appeal: Appeal, textForm: Form[_])(implicit ctx: Context) =
frag(
h1(if (appeal.isOpen) "Ongoing appeal" else "Closed appeal"),
standardFlash(),
@ -52,7 +66,8 @@ object appeal2 {
),
div(cls := "appeal__msg__text")(richText(msg.text))
)
}
},
renderForm(textForm, isNew = false)
)
)
@ -90,12 +105,12 @@ object appeal2 {
private def renderUser(appeal: Appeal, userId: User.ID)(implicit lang: Lang) =
userIdLink((if (appeal isAbout userId) userId else User.lichessId).some)
private def renderForm(form: Form[_])(implicit ctx: Context) =
private def renderForm(form: Form[_], isNew: Boolean)(implicit ctx: Context) =
postForm(action := routes.Appeal.post())(
form3.globalError(form),
form3.group(form("text"), trans.description())(
form3.textarea(_)(rows := 8)
form3.group(form("text"), if (isNew) "Create an appeal" else "Add something to the appeal")(
form3.textarea(_)(rows := 6)
),
form3.action(form3.submit(trans.send()))
form3.submit(trans.send())
)
}

View File

@ -172,7 +172,7 @@ object layout {
metaThemeColor,
st.headTitle {
if (ctx.blind) "lichess"
else if (isProd && !isStage) fullTitle | s"$title • lichess.org"
else if (isProd) fullTitle | s"$title • lichess.org"
else s"[dev] ${fullTitle | s"$title • lichess.dev"}"
},
cssTag("site"),
@ -307,7 +307,7 @@ object layout {
else ctx.isBot option botImage,
a(href := "/")(
"lichess",
span(if (isProd && !isStage) ".org" else ".dev")
span(if (isProd) ".org" else ".dev")
)
),
ctx.blind option h2("Navigation"),

View File

@ -540,6 +540,9 @@ GET /report/:username/cheat-inquiry controllers.Report.currentCheatInquiry(us
# Appeal
GET /appeal controllers.Appeal.home
POST /appeal controllers.Appeal.post
GET /appeal/:username controllers.Appeal.show(username: String)
POST /appeal/:username controllers.Appeal.reply(username: String)
POST /appeal/:username/act/:action controllers.Appeal.act(username: String, action: String)
# Stats
GET /stat/rating/distribution/:perf controllers.Stat.ratingDistribution(perf: String)

View File

@ -10,8 +10,8 @@ case class Appeal(
createdAt: DateTime,
updatedAt: DateTime
) {
def id = _id
def isOpen = status != Appeal.Status.Closed
def id = _id
def isOpen = status != Appeal.Status.Closed
def isAbout(userId: User.ID) = _id == userId
def post(text: String, by: User) =
@ -24,6 +24,10 @@ case class Appeal(
updatedAt = DateTime.now,
status = if (by.id == _id && status == Appeal.Status.Read) Appeal.Status.Unread else status
)
def close = copy(status = Appeal.Status.Closed)
def open = copy(status = Appeal.Status.Read)
def mute = copy(status = Appeal.Status.Muted)
}
object Appeal {

View File

@ -12,6 +12,8 @@ final class AppealApi(
def mine(me: User): Fu[Option[Appeal]] = coll.byId[Appeal](me.id)
def get(user: User) = coll.byId[Appeal](user.id)
def post(text: String, me: User) =
mine(me) flatMap {
case None =>
@ -34,4 +36,18 @@ final class AppealApi(
val appeal = prev.post(text, me)
coll.update.one($id(appeal.id), appeal) inject appeal
}
def reply(text: String, prev: Appeal, mod: User) = {
val appeal = prev.post(text, mod)
coll.update.one($id(appeal.id), appeal) inject appeal
}
def close(appeal: Appeal) =
coll.update.one($id(appeal.id), appeal.close).void
def open(appeal: Appeal) =
coll.update.one($id(appeal.id), appeal.open).void
def mute(appeal: Appeal) =
coll.update.one($id(appeal.id), appeal.mute).void
}

View File

@ -130,5 +130,17 @@ ${Mailgun.txt.serviceNote}
)
}
def onAppealReply(user: User): Funit = {
val body = s"""Hello,
Your appeal has received a response from the moderation team: ${baseUrl}/appeal
$regards
"""
lila.common.Bus.publish(SystemMsg(user.id, body), "msgSystemSend")
funit
}
private def userLang(user: User) = user.realLang | lila.i18n.defaultLang
}

View File

@ -30,6 +30,7 @@ object Permission {
case object SetTitle extends Permission("SET_TITLE", List(UserSpy), "Set/unset title")
case object SetEmail extends Permission("SET_EMAIL", List(UserSpy), "Set email address")
case object SeeReport extends Permission("SEE_REPORT", "See reports")
case object Appeals extends Permission("APPEAL", "Handle appeals")
case object ModLog extends Permission("MOD_LOG", "See mod log")
case object SeeInsight extends Permission("SEE_INSIGHT", "View player insights")
case object PracticeConfig extends Permission("PRACTICE_CONFIG", "Configure practice")
@ -80,6 +81,7 @@ object Permission {
UserSpy,
UserEvaluate,
SeeReport,
Appeals,
ModLog,
SeeInsight,
UserSearch,
@ -185,6 +187,7 @@ object Permission {
),
"Misc mod" -> List(
SeeReport,
Appeals,
UserSearch,
MonitoredMod,
ModNote,