opt-out from messages of a team

pull/6966/head
Thibault Duplessis 2020-07-10 17:06:46 +02:00
parent 1e1aeb4d9d
commit c258d24a6c
9 changed files with 121 additions and 49 deletions

View File

@ -1,6 +1,7 @@
package controllers
import play.api.data.Form
import play.api.data.Forms._
import play.api.libs.json._
import play.api.mvc._
import scala.concurrent.duration._
@ -189,16 +190,18 @@ final class Team(
AuthBody { implicit ctx => implicit me =>
LimitPerWeek(me) {
implicit val req = ctx.body
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
}
)
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
}
)
}
}
@ -235,7 +238,8 @@ final class Team(
},
api = _ => {
implicit val body = ctx.body
forms.apiRequest.bindFromRequest()
forms.apiRequest
.bindFromRequest()
.fold(
newJsonFormError,
msg =>
@ -254,7 +258,8 @@ final class Team(
scoped = implicit req =>
me => {
implicit val lang = reqLang
forms.apiRequest.bindFromRequest()
forms.apiRequest
.bindFromRequest()
.fold(
newJsonFormError,
msg =>
@ -272,6 +277,17 @@ final class Team(
}
)
def subscribe(teamId: String) = {
def doSub(req: Request[_], me: UserModel) =
Form(single("v" -> boolean))
.bindFromRequest()(req)
.fold(_ => funit, v => api.subscribe(teamId, me.id, v))
AuthOrScopedBody(_.Team.Write)(
auth = ctx => me => doSub(ctx.body, me) inject Redirect(routes.Team.show(teamId)),
scoped = req => me => doSub(req, me) inject jsonOkResult
)
}
def requests =
Auth { implicit ctx => me =>
import lila.memo.CacheApi._
@ -290,14 +306,18 @@ final class Team(
AuthBody { implicit ctx => me =>
OptionFuResult(api.requestable(id, me)) { team =>
implicit val req = ctx.body
forms.request.bindFromRequest().fold(
err =>
forms.anyCaptcha map { captcha =>
BadRequest(html.team.request.requestForm(team, err, captcha))
},
setup =>
api.createRequest(team, me, setup.message) inject Redirect(routes.Team.show(team.id)).flashSuccess
)
forms.request
.bindFromRequest()
.fold(
err =>
forms.anyCaptcha map { captcha =>
BadRequest(html.team.request.requestForm(team, err, captcha))
},
setup =>
api.createRequest(team, me, setup.message) inject Redirect(
routes.Team.show(team.id)
).flashSuccess
)
}
}
@ -309,13 +329,15 @@ final class Team(
} yield (teamOption |@| requestOption).tupled) {
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
}
)
forms.processRequest
.bindFromRequest()
.fold(
_ => fuccess(routes.Team.show(team.id).toString),
{
case (decision, url) =>
api.processRequest(team, request, (decision == "accept")) inject url
}
)
}
}
@ -451,16 +473,18 @@ final class Team(
}
private def doPmAll(team: TeamModel, me: UserModel)(implicit req: Request[_]): Either[Form[_], Funit] =
forms.pmAll.bindFromRequest()
forms.pmAll
.bindFromRequest()
.fold(
err => Left(err),
msg =>
Right {
PmAllLimitPerUser(me.id) {
val url = s"${env.net.baseUrl}${routes.Team.show(team.id)}"
val full = s"""$msg
---
You received this message because you are part of the team lichess.org${routes.Team.show(team.id)}."""
env.msg.api.multiPost(me, env.team.memberStream.ids(team, MaxPerSecond(50)), full)
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)
funit // we don't wait for the stream to complete, it would make lichess time out
}(funit)
}

View File

@ -11,6 +11,7 @@ case class TeamInfo(
mine: Boolean,
ledByMe: Boolean,
requestedByMe: Boolean,
subscribed: Boolean,
requests: List[RequestWithUser],
forumPosts: List[MiniForumPost],
tours: TeamInfo.PastAndNext
@ -51,12 +52,14 @@ final class TeamInfoApi(
requests <- (team.enabled && me.exists(m => team.leaders(m.id))) ?? api.requestsWithUsers(team)
mine <- me.??(m => api.belongsTo(team.id, m.id))
requestedByMe <- !mine ?? me.??(m => requestRepo.exists(team.id, m.id))
subscribed <- me.ifTrue(mine) ?? { api.isSubscribed(team, _) }
forumPosts <- forumRecent.team(team.id)
tours <- tournaments(team, 5, 5)
} yield TeamInfo(
mine = mine,
ledByMe = me.exists(m => team.leaders(m.id)),
requestedByMe = requestedByMe,
subscribed = subscribed,
requests = requests,
forumPosts = forumPosts,
tours = tours

View File

@ -99,6 +99,17 @@ object show {
if (info.requestedByMe) strong(beingReviewed())
else ctx.isAuth option joinButton(t)
),
ctx.userId.ifTrue(t.enabled && info.mine) map { myId =>
postForm(
cls := "team-show__subscribe form3",
action := routes.Team.subscribe(t.id)
)(
div(
span(form3.cmnToggle("team-subscribe", "subscribe", checked = info.subscribed)),
label(`for` := "team-subscribe")("Subscribe to team messages")
)
)
},
(info.mine && !info.ledByMe) option
postForm(cls := "quit", action := routes.Team.quit(t.id))(
submitButton(cls := "button button-empty button-red confirm")(quitTeam.txt())

View File

@ -304,6 +304,7 @@ GET /team/:id/users controllers.Team.legacyUsers(id: S
GET /team/:id/tournaments controllers.Team.tournaments(id: String)
GET /team/:id/pm-all controllers.Team.pmAll(id: String)
POST /team/:id/pm-all controllers.Team.pmAllSubmit(id: String)
POST /team/:id/subscribe controllers.Team.subscribe(id: String)
# Team API
GET /api/team/all controllers.Team.apiAll(page: Int ?= 1)
GET /api/team/search controllers.Team.apiSearch(text: String ?= "", page: Int ?= 1)

View File

@ -10,38 +10,48 @@ final class MemberRepo(val coll: Coll)(implicit ec: scala.concurrent.ExecutionCo
import BSONHandlers._
type ID = String
// expensive with thousands of members!
def userIdsByTeam(teamId: ID): Fu[Set[ID]] =
def userIdsByTeam(teamId: Team.ID): Fu[Set[Team.ID]] =
coll.secondaryPreferred.distinctEasy[String, Set]("user", $doc("team" -> teamId))
def teamIdsByUser(userId: User.ID): Fu[Set[ID]] =
coll.distinctEasy[ID, Set]("team", $doc("user" -> userId))
def teamIdsByUser(userId: User.ID): Fu[Set[Team.ID]] =
coll.distinctEasy[Team.ID, Set]("team", $doc("user" -> userId))
def removeByteam(teamId: ID): Funit =
def removeByteam(teamId: Team.ID): Funit =
coll.delete.one(teamQuery(teamId)).void
def removeByUser(userId: User.ID): Funit =
coll.delete.one(userQuery(userId)).void
def exists(teamId: ID, userId: User.ID): Fu[Boolean] =
def exists(teamId: Team.ID, userId: User.ID): Fu[Boolean] =
coll.exists(selectId(teamId, userId))
def add(teamId: ID, userId: User.ID): Funit =
def add(teamId: Team.ID, userId: User.ID): Funit =
coll.insert.one(Member.make(team = teamId, user = userId)).void
def remove(teamId: ID, userId: User.ID): Fu[WriteResult] =
def remove(teamId: Team.ID, userId: User.ID): Fu[WriteResult] =
coll.delete.one(selectId(teamId, userId))
def countByTeam(teamId: ID): Fu[Int] =
def countByTeam(teamId: Team.ID): Fu[Int] =
coll.countSel(teamQuery(teamId))
def filterUserIdsInTeam(teamId: ID, userIds: Set[User.ID]): Fu[Set[User.ID]] =
def filterUserIdsInTeam(teamId: Team.ID, userIds: Set[User.ID]): Fu[Set[User.ID]] =
userIds.nonEmpty ??
coll.distinctEasy[User.ID, Set]("user", $inIds(userIds.map { Member.makeId(teamId, _) }))
def teamQuery(teamId: ID) = $doc("team" -> teamId)
private def selectId(teamId: ID, userId: ID) = $id(Member.makeId(teamId, userId))
private def userQuery(userId: ID) = $doc("user" -> userId)
def isSubscribed(team: Team, user: User): Fu[Boolean] =
!coll.exists(selectId(team.id, user.id) ++ $doc("unsub" -> true))
def subscribe(teamId: Team.ID, userId: User.ID, v: Boolean): Funit =
coll.update
.one(
selectId(teamId, userId),
if (v) $unset("unsub")
else $set("unsub" -> true)
)
.void
def teamQuery(teamId: Team.ID) = $doc("team" -> teamId)
private def selectId(teamId: Team.ID, userId: User.ID) = $id(Member.makeId(teamId, userId))
private def userQuery(userId: User.ID) = $doc("user" -> userId)
}

View File

@ -75,6 +75,10 @@ final class TeamApi(
def mine(me: User): Fu[List[Team]] =
cached teamIdsList me.id flatMap teamRepo.byIdsSortPopular
def isSubscribed = memberRepo.isSubscribed _
def subscribe = memberRepo.subscribe _
def countTeamsOf(me: User) =
cached teamIdsList me.id dmap (_.size)
@ -177,7 +181,7 @@ final class TeamApi(
timeline ! Propagate(TeamJoin(user.id, team.id)).toFollowersOf(user.id)
Bus.publish(JoinTeam(id = team.id, userId = user.id), "team")
}
} recover lila.db.recoverDuplicateKey(_ => ())
} recover lila.db.ignoreDuplicateKey
}
def quit(teamId: Team.ID, me: User): Fu[Option[Team]] =

View File

@ -22,13 +22,17 @@ final class TeamMemberStream(
.mapAsync(1)(userRepo.usersFromSecondary)
.mapConcat(identity)
def ids(team: Team, perSecond: MaxPerSecond): Source[User.ID, _] =
idsBatches(team, perSecond)
def subscribedIds(team: Team, perSecond: MaxPerSecond): Source[User.ID, _] =
idsBatches(team, perSecond, $doc("unsub" $ne true))
.mapConcat(identity)
private def idsBatches(team: Team, perSecond: MaxPerSecond): Source[Seq[User.ID], _] =
private def idsBatches(
team: Team,
perSecond: MaxPerSecond,
selector: Bdoc = $empty
): Source[Seq[User.ID], _] =
memberRepo.coll.ext
.find($doc("team" -> team.id), $doc("user" -> true))
.find($doc("team" -> team.id) ++ selector, $doc("user" -> true))
.sort($sort desc "date")
.batchSize(perSecond.value)
.cursor[Bdoc](ReadPreference.secondaryPreferred)

View File

@ -131,4 +131,13 @@ $section-margin-more: 5vh;
margin: 10px 10px 0 0;
}
}
&__subscribe {
div {
@extend %flex-center;
label {
margin-left: 1rem;
cursor: pointer;
}
}
}
}

View File

@ -874,6 +874,12 @@
function startTeam(cfg) {
lichess.socket = lichess.StrongSocket('/team/' + cfg.id, cfg.socketVersion);
cfg.chat && lichess.makeChat(cfg.chat);
$('#team-subscribe').on('change', function() {
const v = this.checked;
$(this).parents('form').each(function() {
$.post($(this).attr('action'), { v: v });
});
});
}
////////////////