lila/modules/user/src/main/User.scala

309 lines
9.5 KiB
Scala

package lila.user
import scala.concurrent.duration._
import lila.common.{ LightUser, EmailAddress, NormalizedEmailAddress }
import lila.rating.PerfType
import org.joda.time.DateTime
case class User(
id: String,
username: String,
perfs: Perfs,
count: Count,
troll: Boolean = false,
ipBan: Boolean = false,
enabled: Boolean,
roles: List[String],
profile: Option[Profile] = None,
engine: Boolean = false,
booster: Boolean = false,
toints: Int = 0,
playTime: Option[User.PlayTime],
title: Option[Title] = None,
createdAt: DateTime,
seenAt: Option[DateTime],
kid: Boolean,
lang: Option[String],
plan: Plan,
reportban: Boolean = false,
rankban: Boolean = false,
totpSecret: Option[TotpSecret] = None
) extends Ordered[User] {
override def equals(other: Any) = other match {
case u: User => id == u.id
case _ => false
}
override def toString =
s"User $username(${perfs.bestRating}) games:${count.game}${troll ?? " troll"}${engine ?? " engine"}"
def light = LightUser(id = id, name = username, title = title.map(_.value), isPatron = isPatron)
def realNameOrUsername = profileOrDefault.nonEmptyRealName | username
def langs = ("en" :: lang.toList).distinct.sorted
def compare(other: User) = id compareTo other.id
def noTroll = !troll
def canTeam = true
def disabled = !enabled
def usernameWithBestRating = s"$username (${perfs.bestRating})"
def titleUsername = title.fold(username)(t => s"$t $username")
def titleUsernameWithBestRating = title.fold(usernameWithBestRating)(_ + " " + usernameWithBestRating)
def profileOrDefault = profile | Profile.default
def hasGames = count.game > 0
def countRated = count.rated
def hasTitle = title.isDefined
lazy val seenRecently: Boolean = timeNoSee < 2.minutes
def timeNoSee: Duration = seenAt.fold[Duration](Duration.Inf) { s =>
(nowMillis - s.getMillis).millis
}
def everLoggedIn = seenAt.??(createdAt !=)
def lame = booster || engine
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)
}
def lightCount = User.LightCount(light, count.game)
private def bestOf(perfTypes: List[PerfType], nb: Int) =
perfTypes.sortBy { pt =>
-(perfs(pt).nb * PerfType.totalTimeRoughEstimation.get(pt).fold(0)(_.roundSeconds))
} take nb
def best8Perfs: List[PerfType] = bestOf(User.firstRow, 4) ::: bestOf(User.secondRow, 4)
def best6Perfs: List[PerfType] = bestOf(User.firstRow ::: User.secondRow, 6)
def hasEstablishedRating(pt: PerfType) = perfs(pt).established
def isPatron = plan.active
def activePlan: Option[Plan] = if (plan.active) Some(plan) else None
def planMonths: Option[Int] = activePlan.map(_.months)
def createdSinceDays(days: Int) = createdAt isBefore DateTime.now.minusDays(days)
def is(name: String) = id == User.normalize(name)
def isBot = title has Title.BOT
def noBot = !isBot
def rankable = noBot && !rankban
def addRole(role: String) = copy(roles = role :: roles)
}
object User {
type ID = String
type CredentialCheck = ClearPassword => Boolean
case class LoginCandidate(user: User, check: CredentialCheck) {
import LoginCandidate._
def apply(p: PasswordAndToken): Result = {
val res =
if (check(p.password)) user.totpSecret.fold[Result](Success(user)) { tp =>
p.token.fold[Result](MissingTotpToken) { token =>
if (tp verify token) Success(user) else InvalidTotpToken
}
}
else InvalidUsernameOrPassword
lila.mon.user.auth.result(res.success)()
res
}
def option(p: PasswordAndToken): Option[User] = apply(p).toOption
}
object LoginCandidate {
sealed abstract class Result(val toOption: Option[User]) {
def success = toOption.isDefined
}
case class Success(user: User) extends Result(user.some)
case object InvalidUsernameOrPassword extends Result(none)
case object MissingTotpToken extends Result(none)
case object InvalidTotpToken extends Result(none)
}
val anonymous = "Anonymous"
val lichessId = "lichess"
val broadcasterId = "broadcaster"
def isOfficial(userId: ID) = userId == lichessId || userId == broadcasterId
case class GDPRErase(user: User) extends AnyVal
case class Erased(value: Boolean) extends AnyVal
case class LightPerf(user: LightUser, perfKey: String, rating: Int, progress: Int)
case class LightCount(user: LightUser, count: Int)
case class Active(user: User)
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 {
override def toString = "ClearPassword(****)"
}
case class TotpToken(value: String) extends AnyVal
case class PasswordAndToken(password: ClearPassword, token: Option[TotpToken])
case class Speaker(username: String, title: Option[Title], enabled: Boolean, troll: Option[Boolean]) {
def isBot = title has Title.BOT
}
case class PlayTime(total: Int, tv: Int) {
import org.joda.time.Period
def totalPeriod = new Period(total * 1000l)
def tvPeriod = new Period(tv * 1000l)
def nonEmptyTvPeriod = (tv > 0) option tvPeriod
}
implicit def playTimeHandler = reactivemongo.bson.Macros.handler[PlayTime]
// what existing usernames are like
val historicalUsernameRegex = """(?i)[a-z0-9][\w-]{0,28}[a-z0-9]""".r
// what new usernames should be like -- now split into further parts for clearer error messages
val newUsernameRegex = """(?i)[a-z][\w-]{0,28}[a-z0-9]""".r
val newUsernamePrefix = """(?i)[a-z].*""".r
val newUsernameSuffix = """(?i).*[a-z0-9]""".r
val newUsernameChars = """(?i)[\w-]*""".r
def couldBeUsername(str: User.ID) = historicalUsernameRegex.matches(str)
def normalize(username: String) = username.toLowerCase
object BSONFields {
val id = "_id"
val username = "username"
val perfs = "perfs"
val count = "count"
val troll = "troll"
val ipBan = "ipBan"
val enabled = "enabled"
val roles = "roles"
val profile = "profile"
val engine = "engine"
val booster = "booster"
val toints = "toints"
val playTime = "time"
val createdAt = "createdAt"
val seenAt = "seenAt"
val kid = "kid"
val createdWithApiVersion = "createdWithApiVersion"
val lang = "lang"
val title = "title"
def glicko(perf: String) = s"$perfs.$perf.gl"
val email = "email"
val verbatimEmail = "verbatimEmail"
val mustConfirmEmail = "mustConfirmEmail"
val prevEmail = "prevEmail"
val colorIt = "colorIt"
val plan = "plan"
val reportban = "reportban"
val rankban = "rankban"
val salt = "salt"
val bpass = "bpass"
val sha512 = "sha512"
val totpSecret = "totp"
val watchList = "watchList"
val changedCase = "changedCase"
}
import lila.db.BSON
import lila.db.dsl._
import Title.titleBsonHandler
implicit val userBSONHandler = new BSON[User] {
import BSONFields._
import reactivemongo.bson.BSONDocument
private implicit def countHandler = Count.countBSONHandler
private implicit def profileHandler = Profile.profileBSONHandler
private implicit def perfsHandler = Perfs.perfsBSONHandler
private implicit def planHandler = Plan.planBSONHandler
private implicit def totpSecretHandler = TotpSecret.totpSecretBSONHandler
def reads(r: BSON.Reader): User = User(
id = r str id,
username = r str username,
perfs = r.getO[Perfs](perfs) | Perfs.default,
count = r.get[Count](count),
troll = r boolD troll,
ipBan = r boolD ipBan,
enabled = r bool enabled,
roles = ~r.getO[List[String]](roles),
profile = r.getO[Profile](profile),
engine = r boolD engine,
booster = r boolD booster,
toints = r nIntD toints,
playTime = r.getO[PlayTime](playTime),
createdAt = r date createdAt,
seenAt = r dateO seenAt,
kid = r boolD kid,
lang = r strO lang,
title = r.getO[Title](title),
plan = r.getO[Plan](plan) | Plan.empty,
reportban = r boolD reportban,
rankban = r boolD rankban,
totpSecret = r.getO[TotpSecret](totpSecret)
)
def writes(w: BSON.Writer, o: User) = BSONDocument(
id -> o.id,
username -> o.username,
perfs -> o.perfs,
count -> o.count,
troll -> w.boolO(o.troll),
ipBan -> w.boolO(o.ipBan),
enabled -> o.enabled,
roles -> o.roles.some.filter(_.nonEmpty),
profile -> o.profile,
engine -> w.boolO(o.engine),
booster -> w.boolO(o.booster),
toints -> w.intO(o.toints),
playTime -> o.playTime,
createdAt -> o.createdAt,
seenAt -> o.seenAt,
kid -> w.boolO(o.kid),
lang -> o.lang,
title -> o.title,
plan -> o.plan.nonEmpty,
reportban -> w.boolO(o.reportban),
rankban -> w.boolO(o.rankban),
totpSecret -> o.totpSecret
)
}
implicit val speakerHandler = reactivemongo.bson.Macros.handler[Speaker]
private val firstRow: List[PerfType] = List(PerfType.Bullet, PerfType.Blitz, PerfType.Rapid, PerfType.Classical, PerfType.Correspondence)
private val secondRow: List[PerfType] = List(PerfType.UltraBullet, PerfType.Crazyhouse, PerfType.Chess960, PerfType.KingOfTheHill, PerfType.ThreeCheck, PerfType.Antichess, PerfType.Atomic, PerfType.Horde, PerfType.RacingKings)
}