team owners can mass PM all team members

pull/6305/head
Thibault Duplessis 2020-04-04 12:42:53 -06:00
parent 57fa6d32f5
commit e8103b95ee
9 changed files with 130 additions and 56 deletions

View File

@ -146,7 +146,7 @@ final class Clas(
env.clas.api.student.activeWithUsers(clas) flatMap { students =>
val url = routes.Clas.show(clas.id.value).url
val full = if (text contains url) text else s"$text\n\n${env.net.baseUrl}$url"
env.msg.api.multiPost(me, students.map(_.user), full)
env.msg.api.multiPost(me, students.map(_.user.id), full)
} inject
Redirect(routes.Clas.show(clas.id.value)).flashSuccess
)

View File

@ -2,6 +2,7 @@ package controllers
import play.api.libs.json._
import play.api.mvc._
import scala.concurrent.duration._
import lila.api.Context
import lila.app._
@ -65,44 +66,38 @@ final class Team(
}
def edit(id: String) = Auth { implicit ctx => _ =>
OptionFuResult(api team id) { team =>
Owner(team) { fuccess(html.team.form.edit(team, forms edit team)) }
WithOwnedTeam(id) { team =>
fuccess(html.team.form.edit(team, forms edit team))
}
}
def update(id: String) = AuthBody { implicit ctx => implicit me =>
OptionFuResult(api team id) { team =>
Owner(team) {
implicit val req = ctx.body
forms
.edit(team)
.bindFromRequest
.fold(
err => BadRequest(html.team.form.edit(team, err)).fuccess,
data => api.update(team, data, me) inject Redirect(routes.Team.show(team.id))
)
}
def update(id: String) = AuthBody { implicit ctx => me =>
WithOwnedTeam(id) { team =>
implicit val req = ctx.body
forms
.edit(team)
.bindFromRequest
.fold(
err => BadRequest(html.team.form.edit(team, err)).fuccess,
data => api.update(team, data, me) inject Redirect(routes.Team.show(team.id)).flashSuccess
)
}
}
def kickForm(id: String) = Auth { implicit ctx => me =>
OptionFuResult(api team id) { team =>
Owner(team) {
env.team.memberRepo userIdsByTeam team.id map { userIds =>
html.team.admin.kick(team, userIds - me.id)
}
WithOwnedTeam(id) { team =>
env.team.memberRepo userIdsByTeam team.id map { userIds =>
html.team.admin.kick(team, userIds - me.id)
}
}
}
def kick(id: String) = AuthBody { implicit ctx => me =>
OptionFuResult(api team id) { team =>
Owner(team) {
implicit val req = ctx.body
forms.selectMember.bindFromRequest.value ?? { api.kick(team, _, me) } inject Redirect(
routes.Team.show(team.id)
)
}
WithOwnedTeam(id) { team =>
implicit val req = ctx.body
forms.selectMember.bindFromRequest.value ?? { api.kick(team, _, me) } inject Redirect(
routes.Team.show(team.id)
).flashSuccess
}
}
def kickUser(teamId: String, userId: String) = Scoped(_.Team.Write) { _ => me =>
@ -115,23 +110,19 @@ final class Team(
}
def changeOwnerForm(id: String) = Auth { implicit ctx => _ =>
OptionFuResult(api team id) { team =>
Owner(team) {
env.team.memberRepo userIdsByTeam team.id map { userIds =>
html.team.admin.changeOwner(team, userIds - team.createdBy)
}
WithOwnedTeam(id) { team =>
env.team.memberRepo userIdsByTeam team.id map { userIds =>
html.team.admin.changeOwner(team, userIds - team.createdBy)
}
}
}
def changeOwner(id: String) = AuthBody { implicit ctx => me =>
OptionFuResult(api team id) { team =>
Owner(team) {
implicit val req = ctx.body
forms.selectMember.bindFromRequest.value ?? { api.changeOwner(team, _, me) } inject Redirect(
routes.Team.show(team.id)
)
}
WithOwnedTeam(id) { team =>
implicit val req = ctx.body
forms.selectMember.bindFromRequest.value ?? { api.changeOwner(team, _, me) } inject Redirect(
routes.Team.show(team.id)
).flashSuccess
}
}
@ -139,7 +130,7 @@ final class Team(
OptionFuResult(api team id) { team =>
(api delete team) >>
env.mod.logApi.deleteTeam(me.id, team.name, team.description) inject
Redirect(routes.Team all 1)
Redirect(routes.Team all 1).flashSuccess
}
}
@ -161,7 +152,7 @@ final class Team(
},
data =>
api.create(data, me) map { team =>
Redirect(routes.Team.show(team.id)): Result
Redirect(routes.Team.show(team.id)).flashSuccess
}
)
}
@ -175,8 +166,8 @@ final class Team(
auth = ctx =>
me =>
api.join(id, me) flatMap {
case Some(Joined(team)) => Redirect(routes.Team.show(team.id)).fuccess
case Some(Motivate(team)) => Redirect(routes.Team.requestForm(team.id)).fuccess
case Some(Joined(team)) => Redirect(routes.Team.show(team.id)).flashSuccess.fuccess
case Some(Motivate(team)) => Redirect(routes.Team.requestForm(team.id)).flashSuccess.fuccess
case _ => notFound(ctx)
},
scoped = req =>
@ -211,7 +202,7 @@ final class Team(
forms.anyCaptcha map { captcha =>
BadRequest(html.team.request.requestForm(team, err, captcha))
},
setup => api.createRequest(team, setup, me) inject Redirect(routes.Team.show(team.id))
setup => api.createRequest(team, setup, me) inject Redirect(routes.Team.show(team.id)).flashSuccess
)
}
}
@ -221,7 +212,7 @@ final class Team(
requestOption <- api request requestId
teamOption <- requestOption.??(req => env.team.teamRepo.owned(req.team, me.id))
} yield (teamOption |@| requestOption).tupled) {
case (team, request) => {
case (team, request) =>
implicit val req = ctx.body
forms.processRequest.bindFromRequest.fold(
_ => fuccess(routes.Team.show(team.id).toString), {
@ -229,7 +220,6 @@ final class Team(
api.processRequest(team, request, (decision === "accept")) inject url
}
)
}
}
}
@ -237,7 +227,7 @@ final class Team(
auth = ctx =>
me =>
OptionResult(api.quit(id, me)) { team =>
Redirect(routes.Team.show(team.id))
Redirect(routes.Team.show(team.id)).flashSuccess
}(ctx),
scoped = _ =>
me =>
@ -266,15 +256,48 @@ final class Team(
}
}
def pmAll(id: String) = Auth { implicit ctx => _ =>
WithOwnedTeam(id) { team =>
Ok(html.team.admin.pmAll(team, forms.pmAll)).fuccess
}
}
def pmAllSubmit(id: String) = AuthBody { implicit ctx => me =>
WithOwnedTeam(id) { team =>
implicit val req = ctx.body
forms.pmAll.bindFromRequest.fold(
err => BadRequest(html.team.admin.pmAll(team, err)).fuccess,
msg =>
PmAllLimitPerUser(me.id) {
val full = s"""$msg
---
You received this message because you are part of the team lichess.org${routes.Team.show(team.id)}."""
env.team.memberRepo.userIdsByTeam(team.id) flatMap {
env.msg.api.multiPost(me, _, full)
} inject Redirect(routes.Team.show(team.id)).flashSuccess
}
)
}
}
private val PmAllLimitPerUser = new lila.memo.RateLimit[lila.user.User.ID](
credits = 6,
duration = 24 hours,
name = "team pm all per user",
key = "team.pmAll"
)
private def OnePerWeek[A <: Result](me: UserModel)(a: => Fu[A])(implicit ctx: Context): Fu[Result] =
api.hasCreatedRecently(me) flatMap { did =>
if (did && !Granter(_.ManageTeam)(me)) Forbidden(views.html.site.message.teamCreateLimit).fuccess
else a
}
private def Owner(team: TeamModel)(a: => Fu[Result])(implicit ctx: Context): Fu[Result] =
if (ctx.me.??(me => team.isCreator(me.id) || isGranted(_.ManageTeam))) a
else renderTeam(team) map { Forbidden(_) }
private def WithOwnedTeam(teamId: String)(f: TeamModel => Fu[Result])(implicit ctx: Context): Fu[Result] =
OptionFuResult(api team teamId) { team =>
if (ctx.userId.exists(team.isCreator) || isGranted(_.ManageTeam)) f(team)
else renderTeam(team) map { Forbidden(_) }
}
private[controllers] def teamsIBelongTo(me: lila.user.User): Fu[List[LightTeam]] =
api mine me map { _.map(_.light) }

View File

@ -1,5 +1,7 @@
package views.html.team
import play.api.data.Form
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
@ -57,4 +59,35 @@ object admin {
)
}
}
def pmAll(t: lila.team.Team, form: Form[_])(implicit ctx: Context) = {
val title = s"${t.name} - message all members"
views.html.base.layout(
title = title,
moreCss = cssTag("team")
) {
main(cls := "page-menu page-small")(
bits.menu(none),
div(cls := "page-menu__content box box-pad")(
h1(title),
p(
"Send a private message to ALL members of the team.",
br,
"You can use this to call players to join a tournament or a team battle.",
br,
"Players who don't like receiving your messages might leave the team."
),
postForm(cls := "form3", action := routes.Team.pmAllSubmit(t.id))(
form3.group(form("message"), trans.message())(form3.textarea(_)(rows := 10)),
form3.actions(
a(href := routes.Team.show(t.slug))(trans.cancel()),
form3.submit(trans.send())
)
)
)
)
}
}
}

View File

@ -30,6 +30,7 @@ object list {
bits.menu("mine".some),
div(cls := "page-menu__content box")(
h1(myTeams()),
standardFlash(),
table(cls := "slist slist-pad")(
if (teams.size > 0) tbody(teams.map(bits.teamTr(_)))
else noTeam()
@ -65,6 +66,7 @@ object list {
)
)
),
standardFlash(),
table(cls := "slist slist-pad")(
if (teams.nbResults > 0)
tbody(cls := "infinitescroll")(

View File

@ -20,7 +20,7 @@ object request {
views.html.base.layout(
title = title,
moreCss = cssTag("team"),
moreJs = frag(infiniteScrollTag, captchaTag)
moreJs = captchaTag
) {
main(cls := "page-menu page-small")(
bits.menu("requests".some),

View File

@ -52,6 +52,7 @@ object show {
)
),
st.section(cls := "team-show__desc")(
standardFlash(),
richText(t.description),
t.location.map { loc =>
frag(br, trans.location(), ": ", richText(loc))
@ -94,6 +95,16 @@ object show {
strong("Team tournament"),
em("An Arena tournament that only members of your team can join")
)
),
a(
href := routes.Team.pmAll(t.id),
cls := "button button-empty text",
dataIcon := "e"
)(
span(
strong("Message all members"),
em("Send a private message to every member of the team")
)
)
)
),

View File

@ -281,6 +281,8 @@ GET /team/:id/changeOwner controllers.Team.changeOwnerForm(i
POST /team/:id/changeOwner controllers.Team.changeOwner(id: String)
POST /team/:id/close controllers.Team.close(id: String)
GET /team/:id/users controllers.Team.legacyUsers(id: String)
GET /team/:id/pm-all controllers.Team.pmAll(id: String)
POST /team/:id/pm-all controllers.Team.pmAllSubmit(id: String)
GET /api/team/:id/users controllers.Team.users(id: String)
# Analyse

View File

@ -126,12 +126,11 @@ final class MsgApi(
def systemPost(destId: User.ID, text: String) =
post(User.lichessId, destId, text, unlimited = true)
def multiPost(orig: User, dests: List[User], text: String): Funit =
dests
.map { dest =>
post(orig.id, dest.id, text, unlimited = true)
def multiPost(orig: User, dests: Iterable[User.ID], text: String): Funit =
lila.common.Future
.linear(dests) {
post(orig.id, _, text, unlimited = true).logFailure(logger).nevermind
}
.sequenceFu
.void
def recentByForMod(user: User, nb: Int): Fu[List[MsgConvo]] =

View File

@ -75,6 +75,10 @@ final private[team] class DataForm(
def createWithCaptcha = withCaptcha(create)
val pmAll = Form(
single("message" -> text(minLength = 3, maxLength = 9000))
)
private def teamExists(setup: TeamSetup) =
teamRepo.coll.exists($id(Team nameToId setup.trim.name))
}