user mod search: watchlist, regex, levenshtein (wip)
parent
1d63f9e8ab
commit
89071a5c13
|
@ -234,11 +234,13 @@ object Mod extends LilaController {
|
|||
}
|
||||
}
|
||||
|
||||
def search = Secure(_.UserSearch) { implicit ctx => me =>
|
||||
val query = (~get("q")).trim
|
||||
Env.mod.search(query) map { users =>
|
||||
html.mod.search(query, users)
|
||||
}
|
||||
def search = SecureBody(_.UserSearch) { implicit ctx => me =>
|
||||
implicit def req = ctx.body
|
||||
val f = lila.mod.UserSearch.form
|
||||
f.bindFromRequest.fold(
|
||||
err => BadRequest(html.mod.search(err, Nil)).fuccess,
|
||||
query => Env.mod.search(query) map { html.mod.search(f.fill(query), _) }
|
||||
)
|
||||
}
|
||||
|
||||
def chatUser(username: String) = Secure(_.ChatTimeout) { implicit ctx => me =>
|
||||
|
@ -282,16 +284,17 @@ object Mod extends LilaController {
|
|||
val query = rawQuery.trim.split(' ').toList
|
||||
val email = query.headOption.map(EmailAddress.apply) flatMap Env.security.emailAddressValidator.validate
|
||||
val username = query lift 1
|
||||
def tryWith(setEmail: EmailAddress, q: String): Fu[Option[Result]] = Env.mod.search(q) flatMap {
|
||||
case List(user) => (!user.everLoggedIn).?? {
|
||||
lila.mon.user.register.modConfirmEmail()
|
||||
modApi.setEmail(me.id, user.id, setEmail)
|
||||
} >>
|
||||
UserRepo.email(user.id) map { email =>
|
||||
Ok(html.mod.emailConfirm("", user.some, email)).some
|
||||
}
|
||||
case _ => fuccess(none)
|
||||
}
|
||||
def tryWith(setEmail: EmailAddress, q: String): Fu[Option[Result]] =
|
||||
Env.mod.search(lila.mod.UserSearch.exact(q)) flatMap {
|
||||
case List(UserModel.WithEmails(user, _)) => (!user.everLoggedIn).?? {
|
||||
lila.mon.user.register.modConfirmEmail()
|
||||
modApi.setEmail(me.id, user.id, setEmail)
|
||||
} >>
|
||||
UserRepo.email(user.id) map { email =>
|
||||
Ok(html.mod.emailConfirm("", user.some, email)).some
|
||||
}
|
||||
case _ => fuccess(none)
|
||||
}
|
||||
email.?? { em =>
|
||||
tryWith(em.acceptable, em.acceptable.value) orElse {
|
||||
username ?? { tryWith(em.acceptable, _) }
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@(query: String, users: List[User])(implicit ctx: Context)
|
||||
@(form: Form[_], users: List[User.WithEmails])(implicit ctx: Context)
|
||||
|
||||
@title = @{ "Search users" }
|
||||
|
||||
|
@ -9,52 +9,25 @@ active = "search") {
|
|||
<style type="text/css">
|
||||
#mod-search form {
|
||||
margin: 30px 0;
|
||||
text-align: center;
|
||||
}
|
||||
#mod-search form input {
|
||||
padding: 15px 25px;
|
||||
font-size: 1.2em;
|
||||
width: 400px;
|
||||
margin: auto;
|
||||
margin-right: 1em;
|
||||
position: relative;
|
||||
}
|
||||
#mod-search .slist em {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="mod-search" class="content_box">
|
||||
<h1 data-icon="y" class="text">@title</h1>
|
||||
<form class="search" action="@routes.Mod.search" method="GET">
|
||||
<input name="q" placeholder="Search by IP, email, or username" value="@query" />
|
||||
<input name="q" placeholder="Search by IP, email, or username" value="@form("q").value" />
|
||||
@base.form.select(form("as"), lila.mod.UserSearch.asChoices)
|
||||
</form>
|
||||
@if(users.nonEmpty) {
|
||||
<table class="slist">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Games</th>
|
||||
<th>Marks</th>
|
||||
<th>IPban</th>
|
||||
<th>Closed</th>
|
||||
<th>Created</th>
|
||||
<th>Active</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@users.map { u =>
|
||||
<tr>
|
||||
<td>@userLink(u, withBestRating = true, params = "?mod")</td>
|
||||
<td>@u.count.game.localize</td>
|
||||
<td>
|
||||
@if(u.engine){ENGINE}
|
||||
@if(u.booster){BOOSTER}
|
||||
@if(u.troll){SHADOWBAN}
|
||||
</td>
|
||||
<td>@if(u.ipBan){IPBAN}</td>
|
||||
<td>@if(u.disabled){CLOSED}</td>
|
||||
<td>@momentFromNow(u.createdAt)</td>
|
||||
<td>@u.seenAt.map(momentFromNow(_))</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody></table>
|
||||
}
|
||||
@userTable(users)
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
@(users: List[User.WithEmails])(implicit ctx: Context)
|
||||
@if(users.nonEmpty) {
|
||||
<table class="slist">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Games</th>
|
||||
<th>Marks</th>
|
||||
<th>IPban</th>
|
||||
<th>Closed</th>
|
||||
<th>Created</th>
|
||||
<th>Active</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@users.map {
|
||||
case lila.user.User.WithEmails(u, emails) => {
|
||||
<tr>
|
||||
<td>
|
||||
@userLink(u, withBestRating = true, params = "?mod")
|
||||
<em>@emails.list.mkString(", ")</em>
|
||||
</td>
|
||||
</td>
|
||||
<td>@u.count.game.localize</td>
|
||||
<td>
|
||||
@if(u.engine){ENGINE}
|
||||
@if(u.booster){BOOSTER}
|
||||
@if(u.troll){SHADOWBAN}
|
||||
</td>
|
||||
<td>@if(u.ipBan){IPBAN}</td>
|
||||
<td>@if(u.disabled){CLOSED}</td>
|
||||
<td>@momentFromNow(u.createdAt)</td>
|
||||
<td>@u.seenAt.map(momentFromNow(_))</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody></table>
|
||||
}
|
|
@ -130,6 +130,10 @@ trait dsl extends LowPriorityDsl {
|
|||
$doc("$unset" -> $doc((Seq(field) ++ fields).map(k => BSONElement(k, BSONString("")))))
|
||||
}
|
||||
|
||||
def $setBoolOrUnset(field: String, value: Boolean): BSONDocument = {
|
||||
if (value) $set(field -> true) else $unset(field)
|
||||
}
|
||||
|
||||
def $min(item: Producer[BSONElement]): BSONDocument = {
|
||||
$doc("$min" -> $doc(item))
|
||||
}
|
||||
|
|
|
@ -91,10 +91,13 @@ final class Env(
|
|||
historyColl = db(CollectionGamingHistory)
|
||||
)
|
||||
|
||||
lazy val search = new UserSearch(
|
||||
securityApi = securityApi,
|
||||
emailValidator = emailValidator
|
||||
)
|
||||
lazy val search = lila.user.UserRepo.withColl { userColl =>
|
||||
new UserSearch(
|
||||
securityApi = securityApi,
|
||||
emailValidator = emailValidator,
|
||||
userColl = userColl
|
||||
)
|
||||
}
|
||||
|
||||
lazy val inquiryApi = new InquiryApi(reportApi, noteApi, logApi)
|
||||
|
||||
|
|
|
@ -1,18 +1,40 @@
|
|||
package lila.mod
|
||||
|
||||
import play.api.data._
|
||||
import play.api.data.Forms._
|
||||
import play.api.data.validation.Constraints
|
||||
import reactivemongo.api.ReadPreference
|
||||
|
||||
import lila.common.{ EmailAddress, IpAddress }
|
||||
import lila.db.dsl._
|
||||
import lila.user.{ User, UserRepo }
|
||||
import User.{ BSONFields => F }
|
||||
|
||||
final class UserSearch(
|
||||
securityApi: lila.security.SecurityApi,
|
||||
emailValidator: lila.security.EmailAddressValidator
|
||||
emailValidator: lila.security.EmailAddressValidator,
|
||||
userColl: Coll
|
||||
) {
|
||||
|
||||
def apply(query: String): Fu[List[User]] =
|
||||
if (query.isEmpty) fuccess(Nil)
|
||||
else EmailAddress.from(query).map(searchEmail) orElse
|
||||
IpAddress.from(query).map(searchIp) getOrElse
|
||||
(searchUsername(query) zip searchFingerHash(query) map Function.tupled(_ ++ _)) // list concatenation, in case a fingerhash is also someone's username
|
||||
def apply(query: UserSearch.Query): Fu[List[User.WithEmails]] = (~query.as match {
|
||||
case "regex" => userColl.find($doc(
|
||||
F.watchList -> true,
|
||||
$or(
|
||||
F.id $regex query.q,
|
||||
F.email $regex query.q,
|
||||
F.prevEmail $regex query.q
|
||||
)
|
||||
)).hint($doc(F.watchList -> 1))
|
||||
.cursor[User](ReadPreference.secondaryPreferred)
|
||||
.list(100)
|
||||
case "levenshtein" => fuccess(Nil)
|
||||
case _ => // "exact"
|
||||
EmailAddress.from(query.q).map(searchEmail) orElse
|
||||
IpAddress.from(query.q).map(searchIp) getOrElse
|
||||
(searchUsername(query.q) zip searchFingerHash(query.q) map Function.tupled(_ ++ _)) // list concatenation, in case a fingerhash is also someone's username
|
||||
}) flatMap { users =>
|
||||
UserRepo withEmails users.map(_.id)
|
||||
}
|
||||
|
||||
private def searchIp(ip: IpAddress) =
|
||||
securityApi recentUserIdsByIp ip map (_.reverse) flatMap UserRepo.usersFromSecondary
|
||||
|
@ -31,3 +53,22 @@ final class UserSearch(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
object UserSearch {
|
||||
|
||||
val asChoices = List(
|
||||
"exact" -> "Exact match over all users",
|
||||
"regex" -> "Regex match over bad users",
|
||||
"levenshtein" -> "Levenshtein over bad users (expensive)"
|
||||
)
|
||||
val asValues = asChoices.map(_._1)
|
||||
|
||||
case class Query(q: String, as: Option[String])
|
||||
|
||||
def exact(q: String) = Query(q, none)
|
||||
|
||||
val form = Form(mapping(
|
||||
"q" -> nonEmptyText,
|
||||
"as" -> optional(nonEmptyText.verifying(asValues contains _))
|
||||
)(Query.apply)(Query.unapply))
|
||||
}
|
||||
|
|
|
@ -80,6 +80,8 @@ case class User(
|
|||
|
||||
def lameOrTroll = lame || troll
|
||||
|
||||
def watchList = booster || engine || troll || reportban || rankban || ipBan
|
||||
|
||||
def lightPerf(key: String) = perfs(key) map { perf =>
|
||||
User.LightPerf(light, key, perf.intRating, perf.progress)
|
||||
}
|
||||
|
@ -161,7 +163,9 @@ object User {
|
|||
|
||||
case class Active(user: User)
|
||||
|
||||
case class Emails(current: Option[EmailAddress], previous: Option[NormalizedEmailAddress])
|
||||
case class Emails(current: Option[EmailAddress], previous: Option[NormalizedEmailAddress]) {
|
||||
def list = current.toList ::: previous.toList
|
||||
}
|
||||
case class WithEmails(user: User, emails: Emails)
|
||||
|
||||
case class ClearPassword(value: String) extends AnyVal {
|
||||
|
@ -231,6 +235,7 @@ object User {
|
|||
val bpass = "bpass"
|
||||
val sha512 = "sha512"
|
||||
val totpSecret = "totp"
|
||||
val watchList = "watchList"
|
||||
}
|
||||
|
||||
import lila.db.BSON
|
||||
|
|
|
@ -36,7 +36,11 @@ object UserRepo {
|
|||
def byIdsSecondary(ids: Iterable[ID]): Fu[List[User]] = coll.byIds[User](ids, ReadPreference.secondaryPreferred)
|
||||
|
||||
def byEmail(email: NormalizedEmailAddress): Fu[Option[User]] = coll.uno[User]($doc(F.email -> email))
|
||||
def byPrevEmail(email: NormalizedEmailAddress): Fu[List[User]] = coll.find($doc(F.prevEmail -> email)).list[User]()
|
||||
def byPrevEmail(
|
||||
email: NormalizedEmailAddress,
|
||||
readPreference: ReadPreference = ReadPreference.secondaryPreferred
|
||||
): Fu[List[User]] =
|
||||
coll.list[User]($doc(F.prevEmail -> email), readPreference)
|
||||
|
||||
def idByEmail(email: NormalizedEmailAddress): Fu[Option[String]] =
|
||||
coll.primitiveOne[String]($doc(F.email -> email), "_id")
|
||||
|
@ -277,26 +281,31 @@ object UserRepo {
|
|||
}
|
||||
}
|
||||
|
||||
def toggleEngine(id: ID): Funit =
|
||||
private def setWatchList(id: ID): Funit =
|
||||
coll.fetchUpdate[User]($id(id)) { u =>
|
||||
$set("engine" -> !u.engine)
|
||||
$setBoolOrUnset(F.watchList, u.watchList)
|
||||
}
|
||||
|
||||
def setEngine(id: ID, v: Boolean): Funit = coll.updateField($id(id), "engine", v).void
|
||||
def toggleEngine(id: ID): Funit =
|
||||
coll.fetchUpdate[User]($id(id)) { u =>
|
||||
$set(F.engine -> !u.engine)
|
||||
} >> setWatchList(id)
|
||||
|
||||
def setBooster(id: ID, v: Boolean): Funit = coll.updateField($id(id), "booster", v).void
|
||||
def setEngine(id: ID, v: Boolean): Funit = coll.updateField($id(id), "engine", v) >> setWatchList(id)
|
||||
|
||||
def setReportban(id: ID, v: Boolean): Funit = coll.updateField($id(id), "reportban", v).void
|
||||
def setBooster(id: ID, v: Boolean): Funit = coll.updateField($id(id), "booster", v) >> setWatchList(id)
|
||||
|
||||
def setRankban(id: ID, v: Boolean): Funit = coll.updateField($id(id), "rankban", v).void
|
||||
def setReportban(id: ID, v: Boolean): Funit = coll.updateField($id(id), "reportban", v) >> setWatchList(id)
|
||||
|
||||
def setIpBan(id: ID, v: Boolean) = coll.updateField($id(id), "ipBan", v).void
|
||||
def setRankban(id: ID, v: Boolean): Funit = coll.updateField($id(id), "rankban", v) >> setWatchList(id)
|
||||
|
||||
def setIpBan(id: ID, v: Boolean) = coll.updateField($id(id), "ipBan", v) >> setWatchList(id)
|
||||
|
||||
def setKid(user: User, v: Boolean) = coll.updateField($id(user.id), "kid", v)
|
||||
|
||||
def isKid(id: ID) = coll.exists($id(id) ++ $doc("kid" -> true))
|
||||
|
||||
def updateTroll(user: User) = coll.updateField($id(user.id), "troll", user.troll)
|
||||
def updateTroll(user: User) = coll.updateField($id(user.id), "troll", user.troll) >> setWatchList(user.id)
|
||||
|
||||
def isEngine(id: ID): Fu[Boolean] = coll.exists($id(id) ++ engineSelect(true))
|
||||
|
||||
|
@ -380,6 +389,20 @@ object UserRepo {
|
|||
}
|
||||
}
|
||||
|
||||
def withEmails(names: List[String]): Fu[List[User.WithEmails]] =
|
||||
coll.find($inIds(names map normalize))
|
||||
.list[Bdoc](none, ReadPreference.secondaryPreferred).map {
|
||||
_ map { doc =>
|
||||
User.WithEmails(
|
||||
userBSONHandler read doc,
|
||||
User.Emails(
|
||||
current = doc.getAs[EmailAddress](F.verbatimEmail) orElse doc.getAs[EmailAddress](F.email),
|
||||
previous = doc.getAs[NormalizedEmailAddress](F.prevEmail)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def hasEmail(id: ID): Fu[Boolean] = email(id).map(_.isDefined)
|
||||
|
||||
def setBot(user: User): Funit =
|
||||
|
|
Loading…
Reference in New Issue