2013-04-02 09:45:45 -06:00
|
|
|
package lila.team
|
|
|
|
|
2020-04-23 17:47:20 -06:00
|
|
|
import org.joda.time.Period
|
2020-04-29 08:00:34 -06:00
|
|
|
import scala.util.chaining._
|
2020-04-23 17:47:20 -06:00
|
|
|
import play.api.libs.json.{ JsSuccess, Json }
|
|
|
|
import reactivemongo.api.{ Cursor, ReadPreference }
|
2020-04-24 09:29:36 -06:00
|
|
|
import scala.util.Try
|
2020-04-23 17:47:20 -06:00
|
|
|
|
2013-04-04 08:28:52 -06:00
|
|
|
import actorApi._
|
2019-11-26 14:44:08 -07:00
|
|
|
import lila.common.Bus
|
2016-04-01 11:50:57 -06:00
|
|
|
import lila.db.dsl._
|
2020-05-23 10:34:30 -06:00
|
|
|
import lila.hub.actorApi.team.{ CreateTeam, JoinTeam, KickFromTeam }
|
2019-12-13 07:30:20 -07:00
|
|
|
import lila.hub.actorApi.timeline.{ Propagate, TeamCreate, TeamJoin }
|
2020-08-20 17:19:53 -06:00
|
|
|
import lila.hub.LeaderTeam
|
2019-12-23 13:19:37 -07:00
|
|
|
import lila.memo.CacheApi._
|
2020-04-23 17:47:20 -06:00
|
|
|
import lila.mod.ModlogApi
|
2019-12-08 10:35:26 -07:00
|
|
|
import lila.user.{ User, UserRepo }
|
2013-04-02 09:45:45 -06:00
|
|
|
|
2013-04-03 11:12:38 -06:00
|
|
|
final class TeamApi(
|
2019-12-02 21:31:35 -07:00
|
|
|
teamRepo: TeamRepo,
|
|
|
|
memberRepo: MemberRepo,
|
|
|
|
requestRepo: RequestRepo,
|
|
|
|
userRepo: UserRepo,
|
2013-04-03 11:12:38 -06:00
|
|
|
cached: Cached,
|
|
|
|
notifier: Notifier,
|
2019-12-02 21:31:35 -07:00
|
|
|
timeline: lila.hub.actors.Timeline,
|
|
|
|
indexer: lila.hub.actors.TeamSearch,
|
2018-07-05 14:39:57 -06:00
|
|
|
modLog: ModlogApi
|
2019-12-13 18:17:43 -07:00
|
|
|
)(implicit ec: scala.concurrent.ExecutionContext) {
|
2013-04-02 09:45:45 -06:00
|
|
|
|
2016-04-01 22:57:54 -06:00
|
|
|
import BSONHandlers._
|
|
|
|
|
2020-08-26 05:00:53 -06:00
|
|
|
def team(id: Team.ID) = teamRepo byId id
|
2013-05-06 14:49:12 -06:00
|
|
|
|
2020-08-20 17:19:53 -06:00
|
|
|
def leaderTeam(id: Team.ID) = teamRepo.coll.byId[LeaderTeam](id, $doc("name" -> true))
|
|
|
|
|
|
|
|
def lightsByLeader = teamRepo.lightsByLeader _
|
2019-10-04 14:46:43 -06:00
|
|
|
|
2019-12-02 21:31:35 -07:00
|
|
|
def request(id: Team.ID) = requestRepo.coll.byId[Request](id)
|
2013-05-06 14:49:12 -06:00
|
|
|
|
2019-12-31 10:44:30 -07:00
|
|
|
def create(setup: TeamSetup, me: User): Fu[Team] = {
|
2013-04-04 08:28:52 -06:00
|
|
|
val s = setup.trim
|
|
|
|
val team = Team.make(
|
|
|
|
name = s.name,
|
|
|
|
location = s.location,
|
|
|
|
description = s.description,
|
|
|
|
open = s.isOpen,
|
2017-02-14 08:34:07 -07:00
|
|
|
createdBy = me
|
|
|
|
)
|
2019-12-03 09:52:11 -07:00
|
|
|
teamRepo.coll.insert.one(team) >>
|
2019-12-02 21:31:35 -07:00
|
|
|
memberRepo.add(team.id, me.id) >>- {
|
2019-12-13 07:30:20 -07:00
|
|
|
cached invalidateTeamIds me.id
|
|
|
|
indexer ! InsertTeam(team)
|
|
|
|
timeline ! Propagate(
|
|
|
|
TeamCreate(me.id, team.id)
|
|
|
|
).toFollowersOf(me.id)
|
|
|
|
Bus.publish(CreateTeam(id = team.id, name = team.name, userId = me.id), "team")
|
|
|
|
} inject team
|
2013-04-04 08:28:52 -06:00
|
|
|
}
|
|
|
|
|
2020-05-05 22:11:15 -06:00
|
|
|
def update(team: Team, edit: TeamEdit, me: User): Funit =
|
|
|
|
edit.trim pipe { e =>
|
|
|
|
team.copy(
|
|
|
|
location = e.location,
|
|
|
|
description = e.description,
|
|
|
|
open = e.isOpen,
|
|
|
|
chat = e.chat
|
|
|
|
) pipe { team =>
|
|
|
|
teamRepo.coll.update.one($id(team.id), team).void >>
|
|
|
|
!team.leaders(me.id) ?? {
|
|
|
|
modLog.teamEdit(me.id, team.createdBy, team.name)
|
|
|
|
} >>-
|
|
|
|
(indexer ! InsertTeam(team))
|
|
|
|
}
|
2017-01-25 03:41:08 -07:00
|
|
|
}
|
2013-04-04 08:28:52 -06:00
|
|
|
|
2017-01-25 03:41:08 -07:00
|
|
|
def mine(me: User): Fu[List[Team]] =
|
2020-04-26 13:24:10 -06:00
|
|
|
cached teamIdsList me.id flatMap teamRepo.byIdsSortPopular
|
2013-04-04 08:28:52 -06:00
|
|
|
|
2020-07-10 09:06:46 -06:00
|
|
|
def isSubscribed = memberRepo.isSubscribed _
|
|
|
|
|
|
|
|
def subscribe = memberRepo.subscribe _
|
|
|
|
|
2020-05-01 12:54:29 -06:00
|
|
|
def countTeamsOf(me: User) =
|
|
|
|
cached teamIdsList me.id dmap (_.size)
|
|
|
|
|
2017-02-05 04:11:03 -07:00
|
|
|
def hasTeams(me: User): Fu[Boolean] = cached.teamIds(me.id).map(_.value.nonEmpty)
|
2015-09-09 09:32:49 -06:00
|
|
|
|
2020-05-01 13:13:01 -06:00
|
|
|
def countCreatedRecently(me: User): Fu[Int] =
|
|
|
|
teamRepo.countCreatedSince(me.id, Period weeks 1)
|
2013-04-04 08:28:52 -06:00
|
|
|
|
2019-12-13 07:30:20 -07:00
|
|
|
def requestsWithUsers(team: Team): Fu[List[RequestWithUser]] =
|
|
|
|
for {
|
|
|
|
requests <- requestRepo findByTeam team.id
|
|
|
|
users <- userRepo usersFromSecondary requests.map(_.user)
|
|
|
|
} yield requests zip users map {
|
|
|
|
case (request, user) => RequestWithUser(request, user)
|
|
|
|
}
|
2013-04-04 08:28:52 -06:00
|
|
|
|
2019-12-13 07:30:20 -07:00
|
|
|
def requestsWithUsers(user: User): Fu[List[RequestWithUser]] =
|
|
|
|
for {
|
2020-04-26 13:24:10 -06:00
|
|
|
teamIds <- teamRepo enabledTeamIdsByLeader user.id
|
2019-12-13 07:30:20 -07:00
|
|
|
requests <- requestRepo findByTeams teamIds
|
|
|
|
users <- userRepo usersFromSecondary requests.map(_.user)
|
|
|
|
} yield requests zip users map {
|
|
|
|
case (request, user) => RequestWithUser(request, user)
|
|
|
|
}
|
2013-04-04 08:28:52 -06:00
|
|
|
|
2020-05-29 11:17:50 -06:00
|
|
|
def join(teamId: Team.ID, me: User, msg: Option[String]): Fu[Option[Requesting]] =
|
2019-12-02 21:31:35 -07:00
|
|
|
teamRepo.coll.byId[Team](teamId) flatMap {
|
2019-07-16 12:03:10 -06:00
|
|
|
_ ?? { team =>
|
|
|
|
if (team.open) doJoin(team, me) inject Joined(team).some
|
2020-05-29 11:17:50 -06:00
|
|
|
else
|
|
|
|
msg.fold(fuccess[Option[Requesting]](Motivate(team).some)) { txt =>
|
|
|
|
createRequest(team, me, txt) inject Joined(team).some
|
|
|
|
}
|
2019-07-16 12:03:10 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-29 11:17:50 -06:00
|
|
|
def joinApi(
|
|
|
|
teamId: Team.ID,
|
|
|
|
me: User,
|
|
|
|
oAuthAppOwner: Option[User.ID],
|
|
|
|
msg: Option[String]
|
|
|
|
): Fu[Option[Requesting]] =
|
2019-12-02 21:31:35 -07:00
|
|
|
teamRepo.coll.byId[Team](teamId) flatMap {
|
2019-07-16 12:03:10 -06:00
|
|
|
_ ?? { team =>
|
2020-05-06 09:22:01 -06:00
|
|
|
if (team.open || oAuthAppOwner.contains(team.createdBy)) doJoin(team, me) inject Joined(team).some
|
2020-05-29 11:17:50 -06:00
|
|
|
else
|
|
|
|
msg.fold(fuccess[Option[Requesting]](Motivate(team).some)) { txt =>
|
|
|
|
createRequest(team, me, txt) inject Joined(team).some
|
|
|
|
}
|
2019-07-16 12:03:10 -06:00
|
|
|
}
|
|
|
|
}
|
2013-04-04 08:28:52 -06:00
|
|
|
|
2019-12-13 07:30:20 -07:00
|
|
|
def requestable(teamId: Team.ID, user: User): Fu[Option[Team]] =
|
|
|
|
for {
|
|
|
|
teamOption <- teamRepo.coll.byId[Team](teamId)
|
|
|
|
able <- teamOption.??(requestable(_, user))
|
|
|
|
} yield teamOption filter (_ => able)
|
2013-04-04 08:28:52 -06:00
|
|
|
|
2019-12-13 07:30:20 -07:00
|
|
|
def requestable(team: Team, user: User): Fu[Boolean] =
|
|
|
|
for {
|
|
|
|
belongs <- belongsTo(team.id, user.id)
|
|
|
|
requested <- requestRepo.exists(team.id, user.id)
|
|
|
|
} yield !belongs && !requested
|
2013-04-04 08:28:52 -06:00
|
|
|
|
2020-05-29 11:17:50 -06:00
|
|
|
def createRequest(team: Team, user: User, msg: String): Funit =
|
2013-05-11 15:45:39 -06:00
|
|
|
requestable(team, user) flatMap {
|
|
|
|
_ ?? {
|
2020-05-29 11:17:50 -06:00
|
|
|
val request = Request.make(team = team.id, user = user.id, message = msg)
|
2019-12-03 09:52:11 -07:00
|
|
|
requestRepo.coll.insert.one(request).void >>- (cached.nbRequests invalidate team.createdBy)
|
2013-05-11 15:45:39 -06:00
|
|
|
}
|
2013-04-04 08:28:52 -06:00
|
|
|
}
|
|
|
|
|
2019-12-13 07:30:20 -07:00
|
|
|
def processRequest(team: Team, request: Request, accept: Boolean): Funit =
|
|
|
|
for {
|
|
|
|
_ <- requestRepo.coll.delete.one(request)
|
|
|
|
_ = cached.nbRequests invalidate team.createdBy
|
|
|
|
userOption <- userRepo byId request.user
|
2020-05-05 22:11:15 -06:00
|
|
|
_ <-
|
|
|
|
userOption
|
|
|
|
.filter(_ => accept)
|
|
|
|
.??(user => doJoin(team, user) >>- notifier.acceptRequest(team, request))
|
2019-12-13 07:30:20 -07:00
|
|
|
} yield ()
|
2013-04-04 08:28:52 -06:00
|
|
|
|
2020-04-23 17:47:20 -06:00
|
|
|
def deleteRequestsByUserId(userId: User.ID) =
|
2019-12-02 21:31:35 -07:00
|
|
|
requestRepo.getByUserId(userId) flatMap {
|
2017-10-30 09:31:27 -06:00
|
|
|
_.map { request =>
|
2019-12-02 21:31:35 -07:00
|
|
|
requestRepo.remove(request.id) >>
|
2020-04-23 17:47:20 -06:00
|
|
|
teamRepo.leadersOf(request.team).map {
|
|
|
|
_ foreach cached.nbRequests.invalidate
|
|
|
|
}
|
2017-10-30 09:31:27 -06:00
|
|
|
}.sequenceFu
|
|
|
|
}
|
|
|
|
|
2020-05-05 22:11:15 -06:00
|
|
|
def doJoin(team: Team, user: User): Funit =
|
|
|
|
!belongsTo(team.id, user.id) flatMap {
|
|
|
|
_ ?? {
|
|
|
|
memberRepo.add(team.id, user.id) >>
|
|
|
|
teamRepo.incMembers(team.id, +1) >>- {
|
|
|
|
cached invalidateTeamIds user.id
|
|
|
|
timeline ! Propagate(TeamJoin(user.id, team.id)).toFollowersOf(user.id)
|
|
|
|
Bus.publish(JoinTeam(id = team.id, userId = user.id), "team")
|
|
|
|
}
|
2020-07-10 09:06:46 -06:00
|
|
|
} recover lila.db.ignoreDuplicateKey
|
2020-05-05 22:11:15 -06:00
|
|
|
}
|
2013-04-04 08:28:52 -06:00
|
|
|
|
2019-07-16 14:45:14 -06:00
|
|
|
def quit(teamId: Team.ID, me: User): Fu[Option[Team]] =
|
2019-12-02 21:31:35 -07:00
|
|
|
teamRepo.coll.byId[Team](teamId) flatMap {
|
2019-07-16 14:45:14 -06:00
|
|
|
_ ?? { team =>
|
|
|
|
doQuit(team, me.id) inject team.some
|
|
|
|
}
|
|
|
|
}
|
2013-04-04 08:28:52 -06:00
|
|
|
|
2020-04-24 10:19:32 -06:00
|
|
|
def teamsOf(username: String) =
|
|
|
|
cached.teamIdsList(User normalize username) flatMap {
|
|
|
|
teamRepo.coll.byIds[Team](_, ReadPreference.secondaryPreferred)
|
|
|
|
}
|
|
|
|
|
2020-05-05 22:11:15 -06:00
|
|
|
private def doQuit(team: Team, userId: User.ID): Funit =
|
|
|
|
belongsTo(team.id, userId) flatMap {
|
|
|
|
_ ?? {
|
|
|
|
memberRepo.remove(team.id, userId) map { res =>
|
|
|
|
if (res.n == 1) teamRepo.incMembers(team.id, -1)
|
|
|
|
cached.invalidateTeamIds(userId)
|
|
|
|
}
|
2020-04-03 07:42:58 -06:00
|
|
|
}
|
2017-01-25 03:41:08 -07:00
|
|
|
}
|
2013-04-04 08:28:52 -06:00
|
|
|
|
2020-07-23 07:31:00 -06:00
|
|
|
def quitAll(userId: User.ID): Fu[List[Team.ID]] =
|
2020-07-03 02:03:55 -06:00
|
|
|
cached.teamIdsList(userId) flatMap { teamIds =>
|
|
|
|
memberRepo.removeByUser(userId) >>
|
2020-07-23 07:31:00 -06:00
|
|
|
teamIds.map { teamRepo.incMembers(_, -1) }.sequenceFu.void inject teamIds
|
2020-07-03 02:03:55 -06:00
|
|
|
}
|
2013-04-04 08:28:52 -06:00
|
|
|
|
2019-07-08 11:59:39 -06:00
|
|
|
def kick(team: Team, userId: User.ID, me: User): Funit =
|
2018-07-05 14:39:57 -06:00
|
|
|
doQuit(team, userId) >>
|
2020-05-23 10:34:30 -06:00
|
|
|
(!team.leaders(me.id)).?? {
|
2018-07-05 14:39:57 -06:00
|
|
|
modLog.teamKick(me.id, userId, team.name)
|
2020-05-23 10:34:30 -06:00
|
|
|
} >>-
|
|
|
|
Bus.publish(KickFromTeam(teamId = team.id, userId = userId), "teamKick")
|
2013-04-04 08:28:52 -06:00
|
|
|
|
2020-04-23 17:47:20 -06:00
|
|
|
private case class TagifyUser(value: String)
|
|
|
|
implicit private val TagifyUserReads = Json.reads[TagifyUser]
|
|
|
|
|
2020-07-23 03:31:57 -06:00
|
|
|
def setLeaders(team: Team, json: String, by: User, byMod: Boolean): Funit = {
|
2020-04-24 09:29:36 -06:00
|
|
|
val leaders: Set[User.ID] = Try {
|
|
|
|
json.trim.nonEmpty ?? {
|
|
|
|
Json.parse(json).validate[List[TagifyUser]] match {
|
|
|
|
case JsSuccess(users, _) =>
|
|
|
|
users
|
|
|
|
.map(_.value.toLowerCase.trim)
|
|
|
|
.filter(User.lichessId !=)
|
|
|
|
.toSet take 30
|
|
|
|
case _ => Set.empty[User.ID]
|
|
|
|
}
|
2018-07-06 08:55:11 -06:00
|
|
|
}
|
2020-04-24 09:29:36 -06:00
|
|
|
} getOrElse Set.empty
|
2020-05-22 10:56:40 -06:00
|
|
|
memberRepo.filterUserIdsInTeam(team.id, leaders) flatMap { ids =>
|
2020-08-03 02:04:50 -06:00
|
|
|
ids.nonEmpty ?? {
|
|
|
|
if (ids(team.createdBy) || !team.leaders(team.createdBy) || by.id == team.createdBy || byMod) {
|
2020-07-23 03:31:57 -06:00
|
|
|
cached.leaders.put(team.id, fuccess(ids))
|
2020-08-03 02:04:50 -06:00
|
|
|
logger.info(s"valid setLeaders ${team.id}: ${ids mkString ", "} by @${by.id}")
|
2020-07-23 03:31:57 -06:00
|
|
|
teamRepo.setLeaders(team.id, ids).void
|
2020-08-03 02:04:50 -06:00
|
|
|
} else {
|
|
|
|
logger.info(s"invalid setLeaders ${team.id}: ${ids mkString ", "} by @${by.id}")
|
|
|
|
funit
|
2020-07-23 03:31:57 -06:00
|
|
|
}
|
2020-05-23 21:18:43 -06:00
|
|
|
}
|
2020-05-22 10:56:40 -06:00
|
|
|
}
|
2020-04-23 17:47:20 -06:00
|
|
|
}
|
2018-07-03 15:00:32 -06:00
|
|
|
|
2020-06-01 12:33:49 -06:00
|
|
|
def isLeaderOf(leader: User.ID, member: User.ID) =
|
|
|
|
cached.teamIdsList(member) flatMap { teamIds =>
|
|
|
|
teamIds.nonEmpty ?? teamRepo.coll.exists($inIds(teamIds) ++ $doc("leaders" -> leader))
|
|
|
|
}
|
|
|
|
|
2013-04-04 08:28:52 -06:00
|
|
|
def enable(team: Team): Funit =
|
2019-12-02 21:31:35 -07:00
|
|
|
teamRepo.enable(team).void >>- (indexer ! InsertTeam(team))
|
2013-04-04 08:28:52 -06:00
|
|
|
|
2020-08-20 08:33:05 -06:00
|
|
|
def disable(team: Team, by: User): Funit =
|
2020-08-23 01:27:19 -06:00
|
|
|
if (lila.security.Granter(_.ManageTeam)(by) || team.createdBy == by.id || !team.leaders(team.createdBy))
|
2020-08-20 08:33:05 -06:00
|
|
|
teamRepo.disable(team).void >>- (indexer ! RemoveTeam(team.id))
|
|
|
|
else
|
|
|
|
teamRepo.setLeaders(team.id, team.leaders - by.id)
|
2013-04-04 08:28:52 -06:00
|
|
|
|
2013-05-28 07:53:32 -06:00
|
|
|
// delete for ever, with members but not forums
|
2013-04-04 08:28:52 -06:00
|
|
|
def delete(team: Team): Funit =
|
2019-12-03 09:52:11 -07:00
|
|
|
teamRepo.coll.delete.one($id(team.id)) >>
|
2019-12-02 21:31:35 -07:00
|
|
|
memberRepo.removeByteam(team.id) >>-
|
2013-04-04 08:28:52 -06:00
|
|
|
(indexer ! RemoveTeam(team.id))
|
|
|
|
|
2019-07-08 11:59:39 -06:00
|
|
|
def syncBelongsTo(teamId: Team.ID, userId: User.ID): Boolean =
|
2017-01-25 03:41:08 -07:00
|
|
|
cached.syncTeamIds(userId) contains teamId
|
|
|
|
|
2019-07-08 11:59:39 -06:00
|
|
|
def belongsTo(teamId: Team.ID, userId: User.ID): Fu[Boolean] =
|
2020-01-15 18:26:38 -07:00
|
|
|
cached.teamIds(userId) dmap (_ contains teamId)
|
2013-04-09 12:58:34 -06:00
|
|
|
|
2020-04-23 17:47:20 -06:00
|
|
|
def leads(teamId: Team.ID, userId: User.ID): Fu[Boolean] =
|
|
|
|
teamRepo.leads(teamId, userId)
|
2013-05-28 07:53:32 -06:00
|
|
|
|
2019-10-02 04:20:44 -06:00
|
|
|
def filterExistingIds(ids: Set[String]): Fu[Set[Team.ID]] =
|
2020-04-30 10:38:58 -06:00
|
|
|
teamRepo.coll.distinctEasy[Team.ID, Set]("_id", $doc("_id" $in ids), ReadPreference.secondaryPreferred)
|
2019-10-02 04:20:44 -06:00
|
|
|
|
2019-10-02 10:50:09 -06:00
|
|
|
def autocomplete(term: String, max: Int): Fu[List[Team]] =
|
2020-08-21 09:18:23 -06:00
|
|
|
teamRepo.coll
|
2019-12-13 07:30:20 -07:00
|
|
|
.find(
|
|
|
|
$doc(
|
|
|
|
"name".$startsWith(java.util.regex.Pattern.quote(term), "i"),
|
|
|
|
"enabled" -> true
|
|
|
|
)
|
|
|
|
)
|
|
|
|
.sort($sort desc "nbMembers")
|
2020-07-19 09:15:08 -06:00
|
|
|
.cursor[Team](ReadPreference.secondaryPreferred)
|
|
|
|
.list(max)
|
2019-10-02 10:50:09 -06:00
|
|
|
|
2019-07-08 11:59:39 -06:00
|
|
|
def nbRequests(teamId: Team.ID) = cached.nbRequests get teamId
|
2015-04-12 00:18:47 -06:00
|
|
|
|
2020-01-15 18:24:45 -07:00
|
|
|
private[team] def recomputeNbMembers: Funit =
|
2020-08-21 09:18:23 -06:00
|
|
|
teamRepo.coll
|
|
|
|
.find($empty, $id(true).some)
|
2020-01-15 18:24:45 -07:00
|
|
|
.cursor[Bdoc](ReadPreference.secondaryPreferred)
|
2020-05-05 22:11:15 -06:00
|
|
|
.foldWhileM {} { (_, doc) =>
|
|
|
|
(doc.string("_id") ?? recomputeNbMembers) inject Cursor.Cont {}
|
2020-01-15 18:24:45 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
private[team] def recomputeNbMembers(teamId: Team.ID): Funit =
|
|
|
|
memberRepo.countByTeam(teamId) flatMap { nb =>
|
|
|
|
teamRepo.coll.updateField($id(teamId), "nbMembers", nb).void
|
2016-07-21 06:41:34 -06:00
|
|
|
}
|
2013-04-03 11:12:38 -06:00
|
|
|
}
|