protect blog post creation with delay and captcha

to reduce the abuse a little
ublog-markdown-preview
Thibault Duplessis 2021-09-05 14:10:31 +02:00
parent eeada8aeb0
commit 1aa8579025
8 changed files with 69 additions and 15 deletions

View File

@ -54,8 +54,18 @@ final class Ublog(env: Env) extends LilaController(env) {
def form(username: String) = Auth { implicit ctx => me =>
NotForKids {
if (!me.is(username)) Redirect(routes.Ublog.form(me.username)).fuccess
else Ok(html.ublog.form.create(me, env.ublog.form.create)).fuccess
if (env.ublog.api.canBlog(me)) {
if (!me.is(username)) Redirect(routes.Ublog.form(me.username)).fuccess
else
env.ublog.form.anyCaptcha map { captcha =>
Ok(html.ublog.form.create(me, env.ublog.form.create, captcha))
}
} else
Unauthorized(
html.site.message.notYet(
"Please play a few games and wait 2 days before you can create blog posts."
)
).fuccess
}
}
@ -70,7 +80,10 @@ final class Ublog(env: Env) extends LilaController(env) {
env.ublog.form.create
.bindFromRequest()(ctx.body, formBinding)
.fold(
err => BadRequest(html.ublog.form.create(me, err)).fuccess,
err =>
env.ublog.form.anyCaptcha map { captcha =>
BadRequest(html.ublog.form.create(me, err, captcha))
},
data =>
CreateLimitPerUser(me.id, cost = if (me.isVerified) 1 else 3) {
env.ublog.api.create(data, me) map { post =>

View File

@ -61,4 +61,9 @@ object captcha {
)
}
)
def hiddenEmpty(form: lila.common.Form.FormLike) = frag(
form3.hidden(form("gameId")),
form3.hidden(form("move"))
)
}

View File

@ -103,4 +103,7 @@ object message {
apply("Temporarily disabled")(
"Sorry, his feature is temporarily disabled while we figure out a way to bring it back."
)
def notYet(text: String)(implicit ctx: Context) =
apply("Not yet available")(text)
}

View File

@ -6,6 +6,7 @@ import play.api.data.Form
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.common.Captcha
import lila.ublog.UblogForm.UblogPostData
import lila.ublog.UblogPost
import lila.user.User
@ -14,15 +15,16 @@ object form {
import views.html.ublog.{ post => postView }
def create(user: User, f: Form[UblogPostData])(implicit ctx: Context) =
def create(user: User, f: Form[UblogPostData], captcha: Captcha)(implicit ctx: Context) =
views.html.base.layout(
moreCss = cssTag("ublog"),
moreJs = captchaTag,
title = s"${trans.ublog.xBlog.txt(user.username)}${trans.ublog.newPost()}"
) {
main(cls := "box box-pad page page-small ublog-post-form")(
h1(trans.ublog.newPost()),
etiquette,
inner(user, f, none)
inner(user, f, none, captcha.some)
)
}
@ -35,7 +37,7 @@ object form {
main(cls := "box box-pad page page-small ublog-post-form")(
h1(trans.ublog.editYourBlogPost()),
imageForm(user, post),
inner(user, f, post.some),
inner(user, f, post.some, none),
postForm(
cls := "ublog-post-form__delete",
action := routes.Ublog.delete(user.username, post.id.value)
@ -67,8 +69,8 @@ object form {
def formImage(post: UblogPost) = postView.thumbnail(post, _.Small)
private def inner(user: User, form: Form[UblogPostData], post: Option[UblogPost])(implicit
ctx: Context
private def inner(user: User, form: Form[UblogPostData], post: Option[UblogPost], captcha: Option[Captcha])(
implicit ctx: Context
) =
postForm(
cls := "form3",
@ -89,6 +91,9 @@ object form {
trans.ublog.postBody(),
help = frag(markdownAvailable, br, trans.embedsAvailable()).some
)(form3.textarea(_)(rows := 30)),
captcha.fold(views.html.base.captcha.hiddenEmpty(form)) { c =>
views.html.base.captcha(form, c)
},
form3.actions(
a(href := post.fold(routes.Ublog.index(user.username))(views.html.ublog.post.urlOf))(trans.cancel()),
form3.submit(trans.apply())

View File

@ -11,7 +11,8 @@ final class Env(
timeline: lila.hub.actors.Timeline,
picfitApi: lila.memo.PicfitApi,
picfitUrl: lila.memo.PicfitUrl,
ircApi: lila.irc.IrcApi
ircApi: lila.irc.IrcApi,
captcher: lila.hub.actors.Captcher
)(implicit
ec: scala.concurrent.ExecutionContext
) {

View File

@ -99,6 +99,11 @@ final class UblogApi(
coll.delete.one($id(post.id)) >>
picfitApi.deleteByRel(imageRel(post))
def canBlog(u: User) =
!u.isBot && {
(u.count.game > 0 && u.createdSinceDays(2)) || u.hasTitle || u.isVerified || u.isPatron
}
private def paginatorByUser(user: User, live: Boolean, page: Int): Fu[Paginator[UblogPost.PreviewPost]] =
Paginator(
adapter = new Adapter[UblogPost.PreviewPost](

View File

@ -7,28 +7,49 @@ import play.api.data.Forms._
import lila.common.Form.{ cleanNonEmptyText, cleanText, markdownImage }
import lila.user.User
final class UblogForm(markup: UblogMarkup) {
final class UblogForm(markup: UblogMarkup, val captcher: lila.hub.actors.Captcher)(implicit
ec: scala.concurrent.ExecutionContext
) extends lila.hub.CaptchedForm {
import UblogForm._
val create = Form(
private val base =
mapping(
"title" -> cleanNonEmptyText(minLength = 3, maxLength = 100),
"intro" -> cleanNonEmptyText(minLength = 0, maxLength = 2_000),
"markdown" -> cleanNonEmptyText(minLength = 0, maxLength = 100_000).verifying(markdownImage.constraint),
"live" -> boolean
"live" -> boolean,
"gameId" -> text,
"move" -> text
)(UblogPostData.apply)(UblogPostData.unapply)
val create = Form(
base.verifying(captchaFailMessage, validateCaptcha _)
)
def edit(post: UblogPost) =
create.fill(
UblogPostData(title = post.title, intro = post.intro, markdown = post.markdown, live = post.live)
Form(base).fill(
UblogPostData(
title = post.title,
intro = post.intro,
markdown = post.markdown,
live = post.live,
gameId = "",
move = ""
)
)
}
object UblogForm {
case class UblogPostData(title: String, intro: String, markdown: String, live: Boolean) {
case class UblogPostData(
title: String,
intro: String,
markdown: String,
live: Boolean,
gameId: String,
move: String
) {
def create(user: User) = {
val now = DateTime.now

View File

@ -1,4 +1,5 @@
@import '../../../common/css/plugin';
@import '../../../common/css/component/slist';
@import '../../../common/css/form/form3';
@import '../../../common/css/form/captcha';
@import '../ublog/ublog';