auto-unfollow closed/inactive accounts on follow limit reached

expensive, only once a hour per user.

closes #5737
pull/5750/head
Thibault Duplessis 2019-12-13 14:57:04 -06:00
parent e2453c93ee
commit 5ee1734648
4 changed files with 40 additions and 23 deletions

View File

@ -39,16 +39,7 @@ final class Env(
private lazy val repo: RelationRepo = wire[RelationRepo]
lazy val api = new RelationApi(
coll = coll,
repo = repo,
actor = relation,
timeline = timeline,
prefApi = prefApi,
asyncCache = asyncCache,
maxFollow = config.maxFollow,
maxBlock = config.maxBlock
)
lazy val api: RelationApi = wire[RelationApi]
lazy val stream = wire[RelationStream]

View File

@ -3,10 +3,10 @@ package lila.relation
import reactivemongo.api._
import reactivemongo.api.bson._
import scala.concurrent.duration._
import org.joda.time.DateTime
import BSONHandlers._
import lila.common.Bus
import lila.common.config.Max
import lila.db.dsl._
import lila.db.paginator._
import lila.hub.actors
@ -20,8 +20,8 @@ final class RelationApi(
timeline: actors.Timeline,
prefApi: lila.pref.PrefApi,
asyncCache: lila.memo.AsyncCache.Builder,
maxFollow: Max,
maxBlock: Max
userRepo: lila.user.UserRepo,
config: RelationConfig
) {
import RelationRepo.makeId
@ -80,7 +80,7 @@ final class RelationApi(
def countFollowing(userId: ID) = countFollowingCache get userId
def reachedMaxFollowing(userId: ID): Fu[Boolean] = countFollowingCache get userId map (maxFollow <=)
def reachedMaxFollowing(userId: ID): Fu[Boolean] = countFollowingCache get userId map (config.maxFollow <=)
private val countFollowersCache = asyncCache.clearable[ID, Int](
name = "relation.count.followers",
@ -135,7 +135,7 @@ final class RelationApi(
case _ =>
repo.follow(u1, u2) >> limitFollow(u1) >>- {
countFollowersCache.update(u2, 1 +)
countFollowingCache.update(u1, prev => (prev + 1) atMost maxFollow.value)
countFollowingCache.update(u1, prev => (prev + 1) atMost config.maxFollow.value)
reloadOnlineFriends(u1, u2)
timeline ! Propagate(FollowUser(u1, u2)).toFriendsOf(u1).toUsers(List(u2))
Bus.publish(lila.hub.actorApi.relation.Follow(u1, u2), "relation")
@ -144,12 +144,28 @@ final class RelationApi(
}
}
private val limitFollowRateLimiter = new lila.memo.RateLimit[ID](
credits = 1,
duration = 1 hour,
name = "follow limit cleanup",
key = "follow.limit.cleanup"
)
private def limitFollow(u: ID) = countFollowing(u) flatMap { nb =>
(maxFollow < nb) ?? repo.drop(u, true, nb - maxFollow.value)
(config.maxFollow < nb || true) ?? {
limitFollowRateLimiter(u) {
fetchFollowing(u) flatMap userRepo.filterClosedOrInactiveIds(DateTime.now.minusDays(90))
} flatMap {
case Nil => repo.drop(u, true, nb - config.maxFollow.value)
case inactiveIds =>
repo.unfollowMany(u, inactiveIds) >>-
countFollowingCache.update(u, _ - inactiveIds.size)
}
}
}
private def limitBlock(u: ID) = countBlocking(u) flatMap { nb =>
(maxBlock < nb) ?? repo.drop(u, false, nb - maxBlock.value)
(config.maxBlock < nb) ?? repo.drop(u, false, nb - config.maxBlock.value)
}
def block(u1: ID, u2: ID): Funit = (u1 != u2) ?? {

View File

@ -35,7 +35,7 @@ final private class RelationRepo(coll: Coll) {
): Fu[Set[ID]] =
coll
.withReadPreference(rp)
.distinctEasy[String, Set](
.distinctEasy[ID, Set](
"u1",
$doc(
"u2" -> userId,
@ -44,7 +44,7 @@ final private class RelationRepo(coll: Coll) {
)
private def relating(userId: ID, relation: Relation): Fu[Set[ID]] =
coll.distinctEasy[String, Set](
coll.distinctEasy[ID, Set](
"u2",
$doc(
"u1" -> userId,
@ -57,6 +57,9 @@ final private class RelationRepo(coll: Coll) {
def block(u1: ID, u2: ID): Funit = save(u1, u2, Block)
def unblock(u1: ID, u2: ID): Funit = remove(u1, u2)
def unfollowMany(u1: ID, u2s: Iterable[ID]): Funit =
coll.delete.one($inIds(u2s map { makeId(u1, _) })).void
def unfollowAll(u1: ID): Funit = coll.delete.one($doc("u1" -> u1)).void
private def save(u1: ID, u2: ID, relation: Relation): Funit =

View File

@ -239,12 +239,13 @@ final class UserRepo(val coll: Coll) {
coll.primitiveOne[User.PlayTime]($id(id), F.playTime)
val enabledSelect = $doc(F.enabled -> true)
def engineSelect(v: Boolean) = $doc(F.engine -> (if (v) $boolean(true) else $ne(true)))
def trollSelect(v: Boolean) = $doc(F.troll -> (if (v) $boolean(true) else $ne(true)))
val disabledSelect = $doc(F.enabled -> false)
def engineSelect(v: Boolean) = $doc(F.engine -> (if (v) $boolean(true) else $ne(true)))
def trollSelect(v: Boolean) = $doc(F.troll -> (if (v) $boolean(true) else $ne(true)))
def boosterSelect(v: Boolean) = $doc(F.booster -> (if (v) $boolean(true) else $ne(true)))
val goodLadSelect = enabledSelect ++ engineSelect(false) ++ boosterSelect(false)
def stablePerfSelect(perf: String) =
$doc(s"perfs.$perf.gl.d" -> $lt(lila.rating.Glicko.provisionalDeviation))
val goodLadSelect = enabledSelect ++ engineSelect(false) ++ boosterSelect(false)
val goodLadSelectBson = $doc(
F.enabled -> true,
F.engine $ne true,
@ -544,7 +545,7 @@ final class UserRepo(val coll: Coll) {
.find(
$doc(
F.enabled -> true,
"seenAt" $gt since,
F.seenAt $gt since,
"count.game" $gt 9,
"kid" $ne true
),
@ -607,6 +608,12 @@ final class UserRepo(val coll: Coll) {
coll.exists($id(user.id) ++ $doc("erasedAt" $exists true))
} map User.Erased.apply
def filterClosedOrInactiveIds(since: DateTime)(ids: Iterable[ID]): Fu[List[ID]] =
coll.distinctEasy[ID, List](
F.id,
$inIds(ids) ++ $or(disabledSelect, F.seenAt $lt since)
)
private def newUser(
username: String,
passwordHash: HashedPassword,