Merge branch 'master' into puzzle-racer

* master: (131 commits)
  don't show arena user powertips on touchscreens - closes #8350
  not all marks make bad - for lichess-org/tavern#59
  REVERT ME 2: fixup partial explorer hack
  REVERT ME: make partial explorer available again
  Revert "REVERT ME: explain explorer outage due to fire"
  add loose rate limits to following - closes #8352
  code tweaks
  auto-report alt prints - closes lichess-org/tavern#50
  show draw offers during gameplay in move list - closes #4800
  show draw offers in analysis board - for #4800
  show draw offers in exported PGN
  fix storage of draw offers
  only report donations twice a day
  remember all draw offers - WIP
  Updated
  ceval: Cache wasm binary via IndexedDB
  fix anon chat author - closes lichess-org/tavern#51
  prevent double timeout
  link to report FAQ
  link to appeal doc, tweak appeal style
  ...
puzzle-racer-road-translate
Thibault Duplessis 2021-03-12 09:13:06 +01:00
commit d31d9fa864
182 changed files with 1331 additions and 749 deletions

View File

@ -74,9 +74,7 @@ final class Analyse(
pov,
data,
initialFen,
env.analyse
.annotator(pgn, analysis, pov.game.opening, pov.game.winnerColor, pov.game.status)
.toString,
env.analyse.annotator(pgn, pov.game, analysis).toString,
analysis,
analysisInProgress,
simul,
@ -136,9 +134,7 @@ final class Analyse(
html.analyse.replayBot(
pov,
initialFen,
env.analyse
.annotator(pgn, analysis, pov.game.opening, pov.game.winnerColor, pov.game.status)
.toString,
env.analyse.annotator(pgn, pov.game, analysis).toString,
simul,
crosstable
)

View File

@ -82,7 +82,7 @@ final class Appeal(env: Env, reportC: => Report) extends LilaController(env) {
Secure(_.Appeals) { implicit ctx => me =>
asMod(username) { (appeal, suspect) =>
env.appeal.api.toggleMute(appeal) >>
env.report.api.inquiries.toggle(lila.report.Mod(me), appeal.id) inject
env.report.api.inquiries.toggle(lila.report.Mod(me.user), appeal.id) inject
Redirect(routes.Appeal.queue)
}
}

View File

@ -297,8 +297,8 @@ final class Auth(
_ ?? { hash =>
!me.lame ?? (for {
otherIds <- api.recentUserIdsByFingerHash(hash).map(_.filter(me.id.!=))
_ <- (otherIds.sizeIs >= 2) ?? env.user.repo.countEngines(otherIds).flatMap {
case nb if nb >= 2 && nb >= otherIds.size / 2 => env.report.api.autoCheatPrintReport(me.id)
_ <- (otherIds.sizeIs >= 2) ?? env.user.repo.countLameOrTroll(otherIds).flatMap {
case nb if nb >= 2 && nb >= otherIds.size / 2 => env.report.api.autoAltPrintReport(me.id)
case _ => funit
}
} yield ())

View File

@ -8,6 +8,7 @@ import views._
import lila.api.Context
import lila.app._
import lila.user.Holder
final class Clas(
env: Env,
@ -55,7 +56,7 @@ final class Clas(
.fold(
err => BadRequest(html.clas.clas.create(err)).fuccess,
data =>
env.clas.api.clas.create(data, me) map { clas =>
env.clas.api.clas.create(data, me.user) map { clas =>
Redirect(routes.Clas.show(clas.id.value))
}
)
@ -68,7 +69,7 @@ final class Clas(
def show(id: String) =
Auth { implicit ctx => me =>
WithClassAny(id, me)(
forTeacher = WithClass(me, id) { clas =>
forTeacher = WithClass(Holder(me), id) { clas =>
env.clas.api.student.activeWithUsers(clas) map { students =>
preloadStudentUsers(students)
views.html.clas.teacherDashboard.overview(clas, students)
@ -115,7 +116,7 @@ final class Clas(
def wall(id: String) =
Secure(_.Teacher) { implicit ctx => me =>
WithClassAny(id, me)(
WithClassAny(id, me.user)(
forTeacher = WithClass(me, id) { clas =>
env.clas.api.student.allWithUsers(clas) map { students =>
val wall = scalatags.Text.all.raw(env.clas.markup(clas.wall))
@ -265,7 +266,7 @@ final class Clas(
def archive(id: String, v: Boolean) =
SecureBody(_.Teacher) { _ => me =>
WithClass(me, id) { clas =>
env.clas.api.clas.archive(clas, me, v) inject
env.clas.api.clas.archive(clas, me.user, v) inject
Redirect(routes.Clas.show(clas.id.value)).flashSuccess
}
}
@ -322,7 +323,7 @@ final class Clas(
)
},
data =>
env.clas.api.student.create(clas, data, me) map { s =>
env.clas.api.student.create(clas, data, me.user) map { s =>
Redirect(routes.Clas.studentForm(clas.id.value))
.flashing("created" -> s"${s.student.userId} ${s.password.value}")
}
@ -373,7 +374,7 @@ final class Clas(
.fold(
err => BadRequest(html.clas.student.manyForm(clas, students, err, nbStudents)).fuccess,
data =>
env.clas.api.student.manyCreate(clas, data, me) flatMap { many =>
env.clas.api.student.manyCreate(clas, data, me.user) flatMap { many =>
env.user.lightUserApi.preloadMany(many.map(_.student.userId)) inject
Redirect(routes.Clas.studentManyForm(clas.id.value))
.flashing(
@ -595,12 +596,12 @@ final class Clas(
if (students.sizeIs <= lila.clas.Clas.maxStudents) f
else Unauthorized(views.html.clas.teacherDashboard.unreasonable(clas, students, active)).fuccess
private def WithClass(me: lila.user.User, clasId: String)(
private def WithClass(me: Holder, clasId: String)(
f: lila.clas.Clas => Fu[Result]
): Fu[Result] =
env.clas.api.clas.getAndView(lila.clas.Clas.Id(clasId), me) flatMap { _ ?? f }
env.clas.api.clas.getAndView(lila.clas.Clas.Id(clasId), me.user) flatMap { _ ?? f }
private def WithClassAndStudents(me: lila.user.User, clasId: String)(
private def WithClassAndStudents(me: Holder, clasId: String)(
f: (lila.clas.Clas, List[lila.clas.Student]) => Fu[Result]
): Fu[Result] =
WithClass(me, clasId) { c =>

View File

@ -73,7 +73,7 @@ final class Coach(env: Env) extends LilaController(env) {
def approveReview(id: String) =
SecureBody(_.Coach) { implicit ctx => me =>
OptionFuResult(api.reviews.byId(id)) { review =>
api.byId(review.coachId).map(_ ?? (_ is me)) flatMap {
api.byId(review.coachId).dmap(_.exists(_ is me.user)) flatMap {
case false => notFound
case true => api.reviews.approve(review, getBool("v")) inject Ok
}
@ -123,7 +123,7 @@ final class Coach(env: Env) extends LilaController(env) {
}
def pictureApply =
AuthBody(parse.multipartFormData) { implicit ctx => me =>
SecureBody(parse.multipartFormData)(lila.security.Permission.Coach) { implicit ctx => me =>
OptionFuResult(api findOrInit me) { c =>
ctx.body.body.file("picture") match {
case Some(pic) =>

View File

@ -7,6 +7,7 @@ import views._
import lila.app._
import lila.common.{ HTTPRequest, IpAddress }
import lila.user.Holder
final class ForumTopic(env: Env) extends LilaController(env) with ForumController {
@ -78,7 +79,7 @@ final class ForumTopic(env: Env) extends LilaController(env) with ForumControlle
Auth { implicit ctx => me =>
CategGrantMod(categSlug) {
OptionFuRedirect(topicApi.show(categSlug, slug, 1, ctx.me)) { case (categ, topic, pag) =>
topicApi.toggleClose(categ, topic, me) inject
topicApi.toggleClose(categ, topic, Holder(me)) inject
routes.ForumTopic.show(categSlug, slug, pag.nbPages)
}
}
@ -96,7 +97,7 @@ final class ForumTopic(env: Env) extends LilaController(env) with ForumControlle
Auth { implicit ctx => me =>
CategGrantMod(categSlug) {
OptionFuRedirect(topicApi.show(categSlug, slug, 1, ctx.me)) { case (categ, topic, pag) =>
topicApi.toggleSticky(categ, topic, me) inject
topicApi.toggleSticky(categ, topic, Holder(me)) inject
routes.ForumTopic.show(categSlug, slug, pag.nbPages)
}
}

View File

@ -12,6 +12,7 @@ import lila.common.HTTPRequest
import lila.db.dsl._
import lila.api.GameApiV2
import lila.common.config
import lila.user.Holder
final class GameMod(env: Env) extends LilaController(env) {
@ -54,7 +55,7 @@ final class GameMod(env: Env) extends LilaController(env) {
}
}
private def multipleAnalysis(me: lila.user.User, gameIds: Seq[lila.game.Game.ID])(implicit ctx: Context) =
private def multipleAnalysis(me: Holder, gameIds: Seq[lila.game.Game.ID])(implicit ctx: Context) =
env.game.gameRepo.unanalysedGames(gameIds).flatMap { games =>
games.map { game =>
env.fishnet.analyser(
@ -107,7 +108,7 @@ object GameMod {
val emptyFilter = Filter(none, none, none)
def toDbSelect(filter: Filter): Bdoc =
lila.game.Query.notSimul ++ filter.arena.?? { id =>
lila.game.Query.notSimul ++ lila.game.Query.clock(true) ++ filter.arena.?? { id =>
$doc(lila.game.Game.BSONFields.tournamentId -> id)
} ++ filter.swiss.?? { id =>
$doc(lila.game.Game.BSONFields.swissId -> id)

View File

@ -17,7 +17,7 @@ import lila.i18n.I18nLangPicker
import lila.notify.Notification.Notifies
import lila.oauth.{ OAuthScope, OAuthServer }
import lila.security.{ FingerPrintedUser, Granter, Permission }
import lila.user.{ UserContext, User => UserModel }
import lila.user.{ UserContext, User => UserModel, Holder }
abstract private[controllers] class LilaController(val env: Env)
extends BaseController
@ -171,17 +171,17 @@ abstract private[controllers] class LilaController(val env: Env)
}
}
protected def Secure(perm: Permission.Selector)(f: Context => UserModel => Fu[Result]): Action[AnyContent] =
protected def Secure(perm: Permission.Selector)(f: Context => Holder => Fu[Result]): Action[AnyContent] =
Secure(perm(Permission))(f)
protected def Secure(perm: Permission)(f: Context => UserModel => Fu[Result]): Action[AnyContent] =
protected def Secure(perm: Permission)(f: Context => Holder => Fu[Result]): Action[AnyContent] =
Secure(parse.anyContent)(perm)(f)
protected def Secure[A](
parser: BodyParser[A]
)(perm: Permission)(f: Context => UserModel => Fu[Result]): Action[A] =
)(perm: Permission)(f: Context => Holder => Fu[Result]): Action[A] =
Auth(parser) { implicit ctx => me =>
if (isGranted(perm)) f(ctx)(me) else authorizationFailed
if (isGranted(perm)) f(ctx)(Holder(me)) else authorizationFailed
}
protected def SecureF(s: UserModel => Boolean)(f: Context => UserModel => Fu[Result]): Action[AnyContent] =
@ -191,14 +191,14 @@ abstract private[controllers] class LilaController(val env: Env)
protected def SecureBody[A](
parser: BodyParser[A]
)(perm: Permission)(f: BodyContext[A] => UserModel => Fu[Result]): Action[A] =
)(perm: Permission)(f: BodyContext[A] => Holder => Fu[Result]): Action[A] =
AuthBody(parser) { implicit ctx => me =>
if (isGranted(perm)) f(ctx)(me) else authorizationFailed
if (isGranted(perm)) f(ctx)(Holder(me)) else authorizationFailed
}
protected def SecureBody(
perm: Permission.Selector
)(f: BodyContext[_] => UserModel => Fu[Result]): Action[AnyContent] =
)(f: BodyContext[_] => Holder => Fu[Result]): Action[AnyContent] =
SecureBody(parse.anyContent)(perm(Permission))(f)
protected def Scoped[A](
@ -247,26 +247,26 @@ abstract private[controllers] class LilaController(val env: Env)
}
protected def SecureOrScoped(perm: Permission.Selector)(
secure: Context => UserModel => Fu[Result],
scoped: RequestHeader => UserModel => Fu[Result]
secure: Context => Holder => Fu[Result],
scoped: RequestHeader => Holder => Fu[Result]
): Action[Unit] =
Action.async(parse.empty) { req =>
if (HTTPRequest isOAuth req) securedScopedAction(perm, req)(scoped)
else Secure(parse.empty)(perm(Permission))(secure)(req)
}
protected def SecureOrScopedBody(perm: Permission.Selector)(
secure: BodyContext[_] => UserModel => Fu[Result],
scoped: RequestHeader => UserModel => Fu[Result]
secure: BodyContext[_] => Holder => Fu[Result],
scoped: RequestHeader => Holder => Fu[Result]
): Action[AnyContent] =
Action.async(parse.anyContent) { req =>
if (HTTPRequest isOAuth req) securedScopedAction(perm, req.map(_ => ()))(scoped)
else SecureBody(parse.anyContent)(perm(Permission))(secure)(req)
}
private def securedScopedAction(perm: Permission.Selector, req: Request[Unit])(
f: RequestHeader => UserModel => Fu[Result]
f: RequestHeader => Holder => Fu[Result]
) =
Scoped() { req => me =>
IfGranted(perm, req, me)(f(req)(me))
IfGranted(perm, req, me)(f(req)(Holder(me)))
}(req)
def IfGranted(perm: Permission.Selector)(f: => Fu[Result])(implicit ctx: Context): Fu[Result] =

View File

@ -14,7 +14,7 @@ import lila.common.{ EmailAddress, HTTPRequest, IpAddress }
import lila.mod.UserSearch
import lila.report.{ Suspect, Mod => AsMod }
import lila.security.{ FingerHash, Permission }
import lila.user.{ User => UserModel, Title }
import lila.user.{ User => UserModel, Title, Holder }
final class Mod(
env: Env,
@ -26,12 +26,14 @@ final class Mod(
private def modLogApi = env.mod.logApi
private def assessApi = env.mod.assessApi
implicit private def asMod(holder: Holder) = AsMod(holder.user)
def alt(username: String, v: Boolean) =
OAuthModBody(_.CloseAccount) { me =>
withSuspect(username) { sus =>
for {
inquiry <- env.report.api.inquiries ofModId me.id
_ <- modApi.setAlt(AsMod(me), sus, v)
_ <- modApi.setAlt(me, sus, v)
_ <- (v && sus.user.enabled) ?? env.closeAccount(sus.user.id, self = false)
} yield (inquiry, sus).some
}
@ -46,7 +48,7 @@ final class Mod(
withSuspect(username) { sus =>
for {
inquiry <- env.report.api.inquiries ofModId me.id
_ <- modApi.setEngine(AsMod(me), sus, v)
_ <- modApi.setEngine(me, sus, v)
} yield (inquiry, sus).some
}
}(ctx =>
@ -62,12 +64,19 @@ final class Mod(
}
}
def publicChatTimeout =
SecureBody(_.ChatTimeout) { implicit ctx => me =>
FormResult(lila.chat.ChatTimeout.form) { data =>
env.chat.api.userChat.publicTimeout(data, me)
}(ctx.body)
}
def booster(username: String, v: Boolean) =
OAuthModBody(_.MarkBooster) { me =>
withSuspect(username) { prev =>
for {
inquiry <- env.report.api.inquiries ofModId me.id
suspect <- modApi.setBoost(AsMod(me), prev, v)
suspect <- modApi.setBoost(me, prev, v)
} yield (inquiry, suspect).some
}
}(ctx =>
@ -81,7 +90,7 @@ final class Mod(
withSuspect(username) { prev =>
for {
inquiry <- env.report.api.inquiries ofModId me.id
suspect <- modApi.setTroll(AsMod(me), prev, v)
suspect <- modApi.setTroll(me, prev, v)
} yield (inquiry, suspect).some
}
}(ctx =>
@ -96,7 +105,7 @@ final class Mod(
withSuspect(username) { prev =>
for {
inquiry <- env.report.api.inquiries ofModId me.id
suspect <- modApi.setTroll(AsMod(me), prev, prev.user.marks.troll)
suspect <- modApi.setTroll(me, prev, prev.user.marks.troll)
_ <- env.msg.api.systemPost(suspect.user.id, preset.text)
_ <- env.mod.logApi.modMessage(me.id, suspect.user.id, preset.name)
} yield (inquiry, suspect).some
@ -123,7 +132,7 @@ final class Mod(
def disableTwoFactor(username: String) =
Secure(_.DisableTwoFactor) { implicit ctx => me =>
modApi.disableTwoFactor(me.id, username) >> userC.modZoneOrRedirect(username)
modApi.disableTwoFactor(me.id, username) >> userC.modZoneOrRedirect(me, username)
}
def closeAccount(username: String) =
@ -144,14 +153,14 @@ final class Mod(
def reportban(username: String, v: Boolean) =
OAuthMod(_.ReportBan) { _ => me =>
withSuspect(username) { sus =>
modApi.setReportban(AsMod(me), sus, v) map some
modApi.setReportban(me, sus, v) map some
}
}(actionResult(username))
def rankban(username: String, v: Boolean) =
OAuthMod(_.RemoveRanking) { _ => me =>
withSuspect(username) { sus =>
modApi.setRankban(AsMod(me), sus, v) map some
modApi.setRankban(me, sus, v) map some
}
}(actionResult(username))
@ -219,6 +228,7 @@ final class Mod(
if (priv) perms.ViewPrivateComms else perms.Shadowban
} { implicit ctx => me =>
OptionFuOk(env.user.repo named username) { user =>
implicit val renderIp = env.mod.ipRender(me)
env.game.gameRepo
.recentPovsByUserFromSecondary(user, 80)
.mon(_.mod.comm.segment("recentPovs"))
@ -259,6 +269,7 @@ final class Mod(
}
}
html.mod.communication(
me,
user,
(povs zip chats) collect {
case (p, Some(c)) if c.nonEmpty => p -> c
@ -288,7 +299,7 @@ final class Mod(
Secure(_.MarkEngine) { implicit ctx => me =>
OptionFuResult(env.user.repo named username) { user =>
assessApi.refreshAssessOf(user) >>
env.irwin.api.requests.fromMod(Suspect(user), AsMod(me)) >>
env.irwin.api.requests.fromMod(Suspect(user), me) >>
userC.renderModZoneActions(username)
}
}
@ -300,7 +311,7 @@ final class Mod(
val f =
if (isAppeal) env.report.api.inquiries.appeal _
else env.report.api.inquiries.spontaneous _
f(AsMod(me), Suspect(user)) inject {
f(me, Suspect(user)) inject {
if (isAppeal) Redirect(routes.Appeal.show(user.username))
else redirect(user.username, mod = true)
}
@ -343,7 +354,7 @@ final class Mod(
}
def print(fh: String) =
SecureBody(_.PrintBan) { implicit ctx => _ =>
SecureBody(_.ViewPrintNoIP) { implicit ctx => _ =>
val hash = FingerHash(fh)
for {
uids <- env.security.api recentUserIdsByFingerHash hash
@ -360,8 +371,9 @@ final class Mod(
}
def singleIp(ip: String) =
SecureBody(_.IpBan) { implicit ctx => _ =>
IpAddress.from(ip) ?? { address =>
SecureBody(_.ViewPrintNoIP) { implicit ctx => me =>
implicit val renderIp = env.mod.ipRender(me)
env.mod.ipRender.decrypt(ip) ?? { address =>
for {
uids <- env.security.api recentUserIdsByIp address
users <- env.user.repo usersFromSecondary uids.reverse
@ -409,7 +421,7 @@ final class Mod(
_ => BadRequest(html.mod.permissions(user, me)).fuccess,
permissions => {
val newPermissions = Permission(permissions) diff Permission(user.roles)
modApi.setPermissions(AsMod(me), user.username, Permission(permissions)) >> {
modApi.setPermissions(me, user.username, Permission(permissions)) >> {
newPermissions(Permission.Coach) ?? env.security.automaticEmail.onBecomeCoach(user)
} >> {
Permission(permissions).exists(_ is Permission.SeeReport) ?? env.plan.api.setLifetime(user)
@ -493,8 +505,8 @@ final class Mod(
_ ?? f
}
private def OAuthMod[A](perm: Permission.Selector)(f: RequestHeader => UserModel => Fu[Option[A]])(
secure: Context => UserModel => A => Fu[Result]
private def OAuthMod[A](perm: Permission.Selector)(f: RequestHeader => Holder => Fu[Option[A]])(
secure: Context => Holder => A => Fu[Result]
): Action[Unit] =
SecureOrScoped(perm)(
secure = ctx => me => f(ctx.req)(me) flatMap { _ ?? secure(ctx)(me) },
@ -504,8 +516,8 @@ final class Mod(
res.isDefined ?? fuccess(jsonOkResult)
}
)
private def OAuthModBody[A](perm: Permission.Selector)(f: UserModel => Fu[Option[A]])(
secure: BodyContext[_] => UserModel => A => Fu[Result]
private def OAuthModBody[A](perm: Permission.Selector)(f: Holder => Fu[Option[A]])(
secure: BodyContext[_] => Holder => A => Fu[Result]
): Action[AnyContent] =
SecureOrScopedBody(perm)(
secure = ctx => me => f(me) flatMap { _ ?? secure(ctx)(me) },
@ -518,7 +530,7 @@ final class Mod(
private def actionResult(
username: String
)(ctx: Context)(@nowarn("cat=unused") user: UserModel)(@nowarn("cat=unused") res: Any) =
)(ctx: Context)(@nowarn("cat=unused") user: Holder)(@nowarn("cat=unused") res: Any) =
if (HTTPRequest isSynchronousHttp ctx.req) fuccess(redirect(username))
else userC.renderModZoneActions(username)(ctx)
}

View File

@ -1,6 +1,7 @@
package controllers
import play.api.libs.json.Json
import scala.concurrent.duration._
import lila.api.Context
import lila.app._
@ -42,29 +43,40 @@ final class Relation(
)
}
private val FollowLimitPerUser = new lila.memo.RateLimit[lila.user.User.ID](
credits = 150,
duration = 72.hour,
key = "follow.user"
)
def follow(userId: String) =
Auth { implicit ctx => me =>
api.reachedMaxFollowing(me.id) flatMap {
case true =>
env.msg.api
.postPreset(
me.id,
lila.msg.MsgPreset.maxFollow(me.username, env.relation.maxFollow.value)
)
.void
case _ =>
api.follow(me.id, UserModel normalize userId).nevermind >> renderActions(userId, getBool("mini"))
}
FollowLimitPerUser(me.id) {
api.reachedMaxFollowing(me.id) flatMap {
case true =>
env.msg.api
.postPreset(
me.id,
lila.msg.MsgPreset.maxFollow(me.username, env.relation.maxFollow.value)
) inject Ok
case _ =>
api.follow(me.id, UserModel normalize userId).nevermind >> renderActions(userId, getBool("mini"))
}
}(rateLimitedFu)
}
def unfollow(userId: String) =
Auth { implicit ctx => me =>
api.unfollow(me.id, UserModel normalize userId).nevermind >> renderActions(userId, getBool("mini"))
FollowLimitPerUser(me.id) {
api.unfollow(me.id, UserModel normalize userId).nevermind >> renderActions(userId, getBool("mini"))
}(rateLimitedFu)
}
def block(userId: String) =
Auth { implicit ctx => me =>
api.block(me.id, UserModel normalize userId).nevermind >> renderActions(userId, getBool("mini"))
FollowLimitPerUser(me.id) {
api.block(me.id, UserModel normalize userId).nevermind >> renderActions(userId, getBool("mini"))
}(rateLimitedFu)
}
def unblock(userId: String) =

View File

@ -7,7 +7,7 @@ import lila.api.{ BodyContext, Context }
import lila.app._
import lila.common.HTTPRequest
import lila.report.{ Room, Report => ReportModel, Mod => AsMod, Reporter, Suspect }
import lila.user.{ User => UserModel }
import lila.user.{ User => UserModel, Holder }
final class Report(
env: Env,
@ -17,9 +17,11 @@ final class Report(
private def api = env.report.api
implicit private def asMod(holder: Holder) = AsMod(holder.user)
def list =
Secure(_.SeeReport) { implicit ctx => me =>
if (env.streamer.liveStreamApi.isStreaming(me.id) && !getBool("force"))
if (env.streamer.liveStreamApi.isStreaming(me.user.id) && !getBool("force"))
fuccess(Forbidden(html.site.message.streamingMod))
else renderList(me, env.report.modFilters.get(me).fold("all")(_.key))
}
@ -34,7 +36,7 @@ final class Report(
protected[controllers] def getScores =
api.maxScores zip env.streamer.api.approval.countRequests zip env.appeal.api.countUnread
private def renderList(me: UserModel, room: String)(implicit ctx: Context) =
private def renderList(me: Holder, room: String)(implicit ctx: Context) =
api.openAndRecentWithFilter(12, Room(room)) zip
getScores flatMap { case (reports, scores ~ streamers ~ appeals) =>
(env.user.lightUserApi preloadMany reports.flatMap(_.report.userIds)) inject
@ -52,7 +54,7 @@ final class Report(
def inquiry(id: String) =
Secure(_.SeeReport) { _ => me =>
api.inquiries.toggle(AsMod(me), id) flatMap { case (prev, next) =>
api.inquiries.toggle(me, id) flatMap { case (prev, next) =>
prev.filter(_.isAppeal).map(_.user).??(env.appeal.api.unreadById) inject
next.fold(
Redirect {
@ -70,7 +72,7 @@ final class Report(
protected[controllers] def onInquiryClose(
inquiry: Option[ReportModel],
me: UserModel,
me: Holder,
goTo: Option[Suspect],
force: Boolean = false
)(implicit ctx: BodyContext[_]): Fu[Result] =
@ -80,7 +82,7 @@ final class Report(
inquiry match {
case None =>
goTo.fold(Redirect(routes.Report.list).fuccess) { s =>
userC.modZoneOrRedirect(s.user.username)
userC.modZoneOrRedirect(me, s.user.username)
}
case Some(prev) =>
val dataOpt = ctx.body.body match {
@ -98,12 +100,12 @@ final class Report(
def redirectToList = Redirect(routes.Report.listWithFilter(prev.room.key))
if (prev.isAppeal) Redirect(routes.Appeal.queue).fuccess
else if (dataOpt.flatMap(_ get "next").exists(_.headOption contains "1"))
api.inquiries.toggleNext(AsMod(me), prev.room) map {
api.inquiries.toggleNext(me, prev.room) map {
_.fold(redirectToList)(onInquiryStart)
}
else if (force) userC.modZoneOrRedirect(prev.user)
else if (force) userC.modZoneOrRedirect(me, prev.user)
else
api.inquiries.toggle(AsMod(me), prev.id) map { case (prev, next) =>
api.inquiries.toggle(me, prev.id) map { case (prev, next) =>
next
.fold(
if (prev.exists(_.isAppeal)) Redirect(routes.Appeal.queue)
@ -118,7 +120,7 @@ final class Report(
SecureBody(_.SeeReport) { implicit ctx => me =>
api byId id flatMap { inquiry =>
inquiry.filter(_.isAppeal).map(_.user).??(env.appeal.api.readById) >>
api.process(AsMod(me), id) >>
api.process(me, id) >>
onInquiryClose(inquiry, me, none, force = true)
}
}
@ -133,7 +135,7 @@ final class Report(
OptionFuResult(env.user.repo named username) { user =>
api.currentCheatReport(lila.report.Suspect(user)) flatMap {
_ ?? { report =>
api.inquiries.toggle(lila.report.Mod(me), report.id).void
api.inquiries.toggle(me, report.id).void
} inject modC.redirect(username, mod = true)
}
}

View File

@ -11,7 +11,7 @@ import lila.api.Context
import lila.app._
import lila.common.config.MaxPerSecond
import lila.team.{ Requesting, Team => TeamModel }
import lila.user.{ User => UserModel }
import lila.user.{ User => UserModel, Holder }
final class Team(
env: Env,
@ -514,7 +514,7 @@ final class Team(
---
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)
.multiPost(Holder(me), env.team.memberStream.subscribedIds(team, MaxPerSecond(50)), full)
.addEffect { nb =>
lila.mon.msg.teamBulk(team.id).record(nb).unit
}

View File

@ -48,7 +48,7 @@ final class TournamentCrud(env: Env) extends LilaController(env) {
.fold(
err => BadRequest(html.tournament.crud.create(err)).fuccess,
data =>
crud.create(data, me) map { tour =>
crud.create(data, me.user) map { tour =>
Redirect {
if (tour.isTeamBattle) routes.Tournament.teamBattleEdit(tour.id)
else routes.TournamentCrud.edit(tour.id)

View File

@ -19,9 +19,10 @@ import lila.common.paginator.Paginator
import lila.common.{ HTTPRequest, IpAddress }
import lila.game.{ Pov, Game => GameModel }
import lila.rating.PerfType
import lila.socket.UserLagCache
import lila.user.{ User => UserModel }
import lila.security.UserLogins
import lila.socket.UserLagCache
import lila.user.{ User => UserModel, Holder }
import lila.security.Granter
final class User(
env: Env,
@ -323,12 +324,14 @@ final class User(
}
def mod(username: String) =
Secure(_.UserModView) { implicit ctx => _ =>
modZoneOrRedirect(username)
Secure(_.UserModView) { implicit ctx => holder =>
modZoneOrRedirect(holder, username)
}
protected[controllers] def modZoneOrRedirect(username: String)(implicit ctx: Context): Fu[Result] =
if (HTTPRequest isEventSource ctx.req) renderModZone(username)
protected[controllers] def modZoneOrRedirect(holder: Holder, username: String)(implicit
ctx: Context
): Fu[Result] =
if (HTTPRequest isEventSource ctx.req) renderModZone(holder, username)
else fuccess(modC.redirect(username))
private def modZoneSegment(fu: Fu[Frag], name: String, user: UserModel): Source[Frag, _] =
@ -352,11 +355,14 @@ final class User(
}
}
protected[controllers] def renderModZone(username: String)(implicit ctx: Context): Fu[Result] = {
protected[controllers] def renderModZone(holder: Holder, username: String)(implicit
ctx: Context
): Fu[Result] = {
env.user.repo withEmails username orFail s"No such user $username" map {
case UserModel.WithEmails(user, emails) =>
import html.user.{ mod => view }
import lila.app.ui.ScalatagsExtensions.LilaFragZero
implicit val renderIp = env.mod.ipRender(holder)
val nbOthers = getInt("nbOthers") | 100
@ -386,12 +392,12 @@ final class User(
val userLoginsFu = env.security.userLogins(user, nbOthers)
val others = userLoginsFu flatMap { userLogins =>
loginsTableData(user, userLogins, nbOthers) map {
html.user.mod.otherUsers(user, _)
html.user.mod.otherUsers(holder, user, _)
}
}
val identification = userLoginsFu map { spy =>
(isGranted(_.Doxing) || (user.lameOrAlt && !user.hasTitle)) ??
html.user.mod.identification(spy)
val identification = userLoginsFu map { logins =>
Granter.is(_.ViewPrintNoIP)(holder) ??
html.user.mod.identification(holder, logins)
}
val irwin = isGranted(_.MarkEngine) ?? env.irwin.api.reports.withPovs(user).map {
_ ?? { reps =>

View File

@ -86,7 +86,7 @@ object tree {
)
)
),
content = p(
content = frag(
"We define this as using any external assistance to strengthen your knowledge and, or, calculation ability to gain an unfair advantage over your opponent. Some examples would include computer engine assistance, opening books (except for correspondence games), endgame tablebases, and asking another player for help, although these arent the only things we would consider cheating."
).some
)
@ -233,7 +233,12 @@ object tree {
none
)
),
p(cls := "appeal__moderators text", dataIcon := "")(doNotMessageModerators())
div(cls := "appeal__rules")(
p(cls := "text", dataIcon := "")(doNotMessageModerators()),
a(cls := "text", dataIcon := "", href := routes.Page.loneBookmark("appeal"))(
"Read more about the appeal process"
)
)
)
}

View File

@ -1,17 +1,20 @@
package views.html.mod
import controllers.routes
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.common.String.html.richText
import lila.hub.actorApi.shutup.PublicSource
import controllers.routes
import lila.mod.IpRender.RenderIp
import lila.user.{ Holder, User }
object communication {
def apply(
u: lila.user.User,
mod: Holder,
u: User,
players: List[(lila.game.Pov, lila.chat.MixedChat)],
convos: List[lila.msg.MsgConvo],
publicLines: List[lila.shutup.PublicLine],
@ -19,7 +22,7 @@ object communication {
history: List[lila.mod.Modlog],
logins: lila.security.UserLogins.TableData,
priv: Boolean
)(implicit ctx: Context) =
)(implicit ctx: Context, renderIp: RenderIp) =
views.html.base.layout(
title = u.username + " communications",
moreCss = frag(
@ -54,7 +57,7 @@ object communication {
),
isGranted(_.UserModView) option frag(
div(cls := "mod-zone none"),
views.html.user.mod.otherUsers(u, logins)(ctx)(cls := "communication__logins")
views.html.user.mod.otherUsers(mod, u, logins)(ctx, renderIp)(cls := "communication__logins")
),
history.nonEmpty option frag(
h2("Moderation history"),
@ -78,7 +81,7 @@ object communication {
h2("Notes from other users"),
div(cls := "notes")(
notes.map { note =>
(isGranted(_.Doxing) || !note.dox) option
(isGranted(_.Admin) || !note.dox) option
div(userIdLink(note.from.some), " ", momentFromNowOnce(note.date), ": ", richText(note.text))
}
)
@ -129,7 +132,7 @@ object communication {
"author" -> (line.author.toLowerCase == u.id)
)
)(
userIdLink(line.author.toLowerCase.some, withOnline = false, withTitle = false),
userIdLink(line.userIdMaybe, withOnline = false, withTitle = false),
nbsp,
richText(line.text)
)

View File

@ -130,7 +130,7 @@ object games {
)
}
),
td(dataSort := pov.game.playedTurns)(pov.game.playedTurns),
td(dataSort := pov.moves)(pov.moves),
td(dataSort := ~pov.player.ratingDiff)(
pov.win match {
case Some(true) => goodTag(cls := "result")("1")

View File

@ -54,7 +54,7 @@ object inquiry {
)
def renderNote(r: lila.user.Note)(implicit ctx: Context) =
(!r.dox || isGranted(_.Doxing)) option div(cls := "doc note")(
(!r.dox || isGranted(_.Admin)) option div(cls := "doc note")(
h3("by ", userIdLink(r.from.some, withOnline = false), ", ", momentFromNow(r.date)),
p(richText(r.text, expandImg = false))
)
@ -239,16 +239,18 @@ object inquiry {
.withFilter(_.byLichess)
.flatMap(_.text.linesIterator)
.collect {
case farmWithRegex(userId) => userId
case sandbagWithRegex(userId) => userId
case farmWithRegex(userId) => List(userId)
case sandbagWithRegex(userIds) => userIds.split(' ').toList.map(_.trim.replace("@", ""))
}
.flatten
.distinct
.toNel
}
private val farmWithRegex =
("^Boosting: farms rating points from @(" + User.historicalUsernameRegex.pattern + ")").r.unanchored
private val sandbagWithRegex =
("^Sandbagging: throws games to @(" + User.historicalUsernameRegex.pattern + ")").r.unanchored
"^Sandbagging: throws games to (.+)".r.unanchored
private def thenForms(url: String, button: Tag) =
div(

View File

@ -3,14 +3,14 @@ package views.html.mod
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.user.User
import lila.user.{ Holder, User }
import lila.security.Permission
import controllers.routes
object permissions {
def apply(u: User, me: User)(implicit ctx: Context) =
def apply(u: User, me: Holder)(implicit ctx: Context) =
views.html.base.layout(
title = s"${u.username} permissions",
moreCss = frag(

View File

@ -6,36 +6,30 @@ import lila.app.ui.ScalatagsTemplate._
import lila.common.String.html.richText
import controllers.routes
import play.api.mvc.Call
import lila.chat.UserChat
import lila.chat.ChatTimeout
object publicChat {
def apply(
tourChats: List[(lila.tournament.Tournament, lila.chat.UserChat)],
simulChats: List[(lila.simul.Simul, lila.chat.UserChat)]
tourChats: List[(lila.tournament.Tournament, UserChat)],
simulChats: List[(lila.simul.Simul, UserChat)]
)(implicit ctx: Context) =
views.html.base.layout(
title = "Public Chats",
moreCss = cssTag("mod.communication"),
moreJs = jsTag("public-chat.js")
moreCss = cssTag("mod.publicChats"),
moreJs = jsModule("publicChats")
) {
main(cls := "page-menu")(
views.html.mod.menu("public-chat"),
div(id := "comm-wrap")(
div(id := "communication", cls := "page-menu__content public_chat box box-pad")(
div(id := "communication", cls := "page-menu__content public-chat box box-pad")(
h2("Tournament Chats"),
div(cls := "player_chats")(
tourChats.map { case (tournament, chat) =>
div(cls := "game")(
a(cls := "title", href := routes.Tournament.show(tournament.id))(tournament.name),
div(cls := "chat")(
chat.lines.filter(_.isVisible).map { line =>
div(cls := "line")(
userIdLink(line.author.toLowerCase.some, withOnline = false, withTitle = false),
" ",
richText(line.text, expandImg = false)
)
}
)
div(cls := "game", dataChan := "tournament", dataRoom := tournament.id)(
chatOf(routes.Tournament.show(tournament.id), tournament.name, chat)
)
}
),
@ -43,23 +37,45 @@ object publicChat {
h2("Simul Chats"),
div(cls := "player_chats")(
simulChats.map { case (simul, chat) =>
div(cls := "game")(
a(cls := "title", href := routes.Simul.show(simul.id))(simul.name),
div(cls := "chat")(
chat.lines.filter(_.isVisible).map { line =>
div(cls := "line")(
userIdLink(line.author.toLowerCase.some, withOnline = false, withTitle = false),
" ",
richText(line.text, expandImg = false)
)
}
)
div(cls := "game", dataChan := "simul", dataRoom := simul.id)(
chatOf(routes.Simul.show(simul.id), simul.name, chat)
)
}
)
),
div(cls := "timeout-modal none")(
h2(cls := "username")("username"),
p(cls := "text")("text"),
div(cls := "continue-with")(
ChatTimeout.Reason.all.map { reason =>
button(cls := "button", value := reason.key)(reason.shortName)
}
)
)
)
)
)
}
private val dataRoom = attr("data-room")
private val dataChan = attr("data-chan")
private def chatOf(url: Call, name: String, chat: UserChat)(implicit ctx: Context) =
frag(
a(cls := "title", href := url)(name),
div(cls := "chat")(
chat.lines.filter(_.isVisible).map { line =>
div(
cls := List(
"line" -> true,
"lichess" -> line.isLichess
)
)(
userIdLink(line.author.toLowerCase.some, withOnline = false, withTitle = false),
" ",
richText(line.text, expandImg = false)
)
}
)
)
}

View File

@ -7,6 +7,7 @@ import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.common.IpAddress
import lila.security.FingerHash
import lila.mod.IpRender.RenderIp
import controllers.routes
@ -80,7 +81,7 @@ object search {
users: List[lila.user.User.WithEmails],
uas: List[String],
blocked: Boolean
)(implicit ctx: Context) =
)(implicit ctx: Context, renderIp: RenderIp) =
views.html.base.layout(
title = "IP address",
moreCss = cssTag("mod.misc")
@ -89,7 +90,7 @@ object search {
views.html.mod.menu("search"),
div(cls := "mod-search page-menu__content box")(
div(cls := "box__top")(
h1("IP address: ", address.value),
h1("IP address: ", renderIp(address)),
postForm(cls := "box__top__actions", action := routes.Mod.singleIpBan(!blocked, address.value))(
submitButton(
cls := List(
@ -99,7 +100,7 @@ object search {
)(if (blocked) "Banned" else "Ban this IP")
)
),
div(cls := "box__pad")(
isGranted(_.Admin) option div(cls := "box__pad")(
h2("User agents"),
ul(uas map { ua =>
li(ua)
@ -151,7 +152,7 @@ object search {
tr(
td(
userLink(u, withBestRating = true, params = "?mod"),
(isGranted(_.Doxing) && isGranted(_.SetEmail)) option
(isGranted(_.Admin) && isGranted(_.SetEmail)) option
email(emails.list.map(_.value).mkString(", "))
),
td(u.count.game.localize),

View File

@ -51,6 +51,11 @@ object form {
private def inner(form: Form[Data], url: play.api.mvc.Call)(implicit ctx: Context) =
postForm(cls := "form3", action := url)(
div(cls := "form-group")(
a(dataIcon := "", cls := "text", href := routes.Page.loneBookmark("broadcasts"))(
"How to use Lichess Broadcasts"
)
),
form3.globalError(form),
form3.group(form("name"), eventName())(form3.input(_)(autofocus)),
form3.group(form("description"), eventDescription())(form3.textarea(_)(rows := 2)),

View File

@ -25,6 +25,11 @@ object form {
cls := "form3",
action := s"${routes.Report.create}${reqUser.??(u => "?username=" + u.username)}"
)(
div(cls := "form-group")(
a(href := routes.Page.loneBookmark("report-faq"), dataIcon := "", cls := "text")(
"Read more about Lichess reports"
)
),
form3.globalError(form),
form3.group(form("username"), trans.user(), klass = "field_to complete-parent") { f =>
reqUser

View File

@ -6,6 +6,7 @@ import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.report.Report.WithSuspect
import lila.user.Holder
object list {
@ -111,7 +112,7 @@ object list {
scoreTag(scores.highest)
),
ctx.me ?? { me =>
lila.report.Room.all.filter(lila.report.Room.isGrantedFor(me)).map { room =>
lila.report.Room.all.filter(lila.report.Room.isGrantedFor(Holder(me))).map { room =>
a(
href := routes.Report.listWithFilter(room.key),
cls := List(

View File

@ -312,6 +312,9 @@ object faq {
whyAreRatingHigher.txt(),
p(
whyAreRatingHigherExplanation()
),
p(
a(href := routes.Page.loneBookmark("rating-systems"))("More about rating systems")
)
),
question(

View File

@ -95,7 +95,7 @@ object bits extends Context.ToLang {
ul(
li(rule1()),
li(rule2()),
li(rule3())
li(a(href := routes.Page.loneBookmark("streamer-page-activation"))(rule3()))
),
h2(perks()),
ul(

View File

@ -28,8 +28,7 @@ object form {
form3.group(form("name"), trans.name())(form3.input(_)),
requestField(form),
passwordField(form),
form3.group(form("location"), trans.location())(form3.input(_)),
form3.group(form("description"), trans.description())(form3.textarea(_)(rows := 10)),
textFields(form),
views.html.base.captcha(form, captcha),
form3.actions(
a(href := routes.Team.home(1))(trans.cancel()),
@ -54,8 +53,7 @@ object form {
),
requestField(form),
passwordField(form),
form3.group(form("location"), trans.location())(form3.input(_)),
form3.group(form("description"), trans.description())(form3.textarea(_)(rows := 10)),
textFields(form),
form3.group(form("chat"), frag("Team chat")) { f =>
form3.select(
f,
@ -94,6 +92,14 @@ object form {
}
}
private def textFields(form: Form[_])(implicit ctx: Context) = frag(
form3.group(form("location"), trans.location())(form3.input(_)),
form3.group(form("description"), trans.description())(form3.textarea(_)(rows := 10)),
form3.group(form("descPrivate"), trans.descPrivate(), help = trans.descPrivateHelp().some)(
form3.textarea(_)(rows := 10)
)
)
private def requestField(form: Form[_])(implicit lang: Lang) =
form3.checkbox(
form("request"),

View File

@ -184,7 +184,9 @@ object show {
div(cls := "team-show__content__col2")(
standardFlash(),
st.section(cls := "team-show__desc")(
markdownLinksOrRichText(t.description),
markdownLinksOrRichText {
t.descPrivate.ifTrue(info.mine) | t.description
},
t.location.map { loc =>
frag(br, trans.location(), ": ", richText(loc))
}

View File

@ -9,8 +9,10 @@ import lila.app.ui.ScalatagsTemplate._
import lila.evaluation.Display
import lila.mod.ModPresets
import lila.playban.RageSit
import lila.security.Granter
import lila.security.{ Permission, UserLogins }
import lila.user.User
import lila.user.{ Holder, User }
import lila.mod.IpRender.RenderIp
object mod {
private def mzSection(key: String) = div(id := s"mz_$key", cls := "mz-section")
@ -44,6 +46,13 @@ object mod {
submitButton(cls := "btn-rack__btn")("Evaluate")
)
},
isGranted(_.Hunter) option {
a(
cls := "btn-rack__btn",
href := routes.GameMod.index(u.username),
title := "View games"
)("Games")
},
isGranted(_.Shadowban) option {
a(
cls := "btn-rack__btn",
@ -190,7 +199,7 @@ object mod {
)
)
},
(isGranted(_.Doxing) && isGranted(_.SetEmail)) ?? frag(
(isGranted(_.Admin) && isGranted(_.SetEmail)) ?? frag(
postForm(cls := "email", action := routes.Mod.setEmail(u.username))(
st.input(
tpe := "email",
@ -516,7 +525,10 @@ object mod {
if (nb > 0) td(cls := "i", dataSort := nb)(content)
else td
def otherUsers(u: User, data: UserLogins.TableData)(implicit ctx: Context): Tag = {
def otherUsers(mod: Holder, u: User, data: UserLogins.TableData)(implicit
ctx: Context,
renderIp: RenderIp
): Tag = {
import data._
mzSection("others")(
table(cls := "slist")(
@ -529,7 +541,7 @@ object mod {
a(cls := "more-others")("Load more")
)
),
th("Email"),
isGranted(_.Admin) option th("Email"),
sortNumberTh("Same"),
th("Games"),
sortNumberTh(playban)(cls := "i", title := "Playban"),
@ -547,17 +559,16 @@ object mod {
),
tbody(
othersWithEmail.others.map { case other @ UserLogins.OtherUser(o, _, _) =>
val dox = isGranted(_.Doxing) || (o.lameOrAlt && !o.hasTitle)
val userNotes =
notes.filter(n => n.to == o.id && (ctx.me.exists(n.isFrom) || isGranted(_.Doxing)))
notes.filter(n => n.to == o.id && (ctx.me.exists(n.isFrom) || isGranted(_.Admin)))
tr(
dataTags := s"${other.ips.mkString(" ")} ${other.fps.mkString(" ")}",
dataTags := s"${other.ips.map(renderIp).mkString(" ")} ${other.fps.mkString(" ")}",
cls := (o == u) option "same"
)(
if (dox || o == u) td(dataSort := o.id)(userLink(o, withBestRating = true, params = "?mod"))
else td,
if (dox) td(othersWithEmail emailValueOf o)
if (o == u || Granter.canViewAltUsername(mod, o))
td(dataSort := o.id)(userLink(o, withBestRating = true, params = "?mod"))
else td,
isGranted(_.Admin) option td(othersWithEmail emailValueOf o),
td(
// show prints and ips separately
dataSort := other.score + (other.ips.nonEmpty ?? 1000000) + (other.fps.nonEmpty ?? 3000000)
@ -602,7 +613,10 @@ object mod {
)
}
def identification(spy: UserLogins)(implicit ctx: Context): Frag = {
def identification(mod: Holder, logins: UserLogins)(implicit
ctx: Context,
renderIp: RenderIp
): Frag = {
val canIpBan = isGranted(_.IpBan)
val canFpBan = isGranted(_.PrintBan)
mzSection("identification")(
@ -617,7 +631,7 @@ object mod {
)
),
tbody(
spy.distinctLocations.toList
logins.distinctLocations.toList
.sortBy(-_.seconds)
.map { loc =>
tr(
@ -635,7 +649,7 @@ object mod {
table(cls := "slist slist--sort")(
thead(
tr(
th(pluralize("Device", spy.uas.size)),
th(pluralize("Device", logins.uas.size)),
th("OS"),
th("Client"),
sortNumberTh("Date"),
@ -643,7 +657,7 @@ object mod {
)
),
tbody(
spy.uas
logins.uas
.sortBy(-_.seconds)
.map { ua =>
import ua.value.client._
@ -666,7 +680,7 @@ object mod {
table(cls := "slist spy_filter slist--sort")(
thead(
tr(
th(pluralize("IP", spy.prints.size)),
th(pluralize("IP", logins.prints.size)),
sortNumberTh("Alts"),
th,
sortNumberTh("Date"),
@ -674,9 +688,10 @@ object mod {
)
),
tbody(
spy.ips.sortBy(ip => (-ip.alts.score, -ip.ip.seconds)).map { ip =>
logins.ips.sortBy(ip => (-ip.alts.score, -ip.ip.seconds)).map { ip =>
val renderedIp = renderIp(ip.ip.value)
tr(cls := ip.blocked option "blocked")(
td(a(href := routes.Mod.singleIp(ip.ip.value.value))(ip.ip.value)),
td(a(href := routes.Mod.singleIp(renderedIp))(renderedIp)),
td(dataSort := ip.alts.score)(altMarks(ip.alts)),
td(ip.proxy option span(cls := "proxy")("PROXY")),
td(dataSort := ip.ip.date.getMillis)(momentFromNowServer(ip.ip.date)),
@ -698,14 +713,14 @@ object mod {
table(cls := "slist spy_filter slist--sort")(
thead(
tr(
th(pluralize("Print", spy.prints.size)),
th(pluralize("Print", logins.prints.size)),
sortNumberTh("Alts"),
sortNumberTh("Date"),
canFpBan option sortNumberTh
)
),
tbody(
spy.prints.sortBy(fp => (-fp.alts.score, -fp.fp.seconds)).map { fp =>
logins.prints.sortBy(fp => (-fp.alts.score, -fp.fp.seconds)).map { fp =>
tr(cls := fp.banned option "blocked")(
td(a(href := routes.Mod.print(fp.fp.value.value))(fp.fp.value)),
td(dataSort := fp.alts.score)(altMarks(fp.alts)),

View File

@ -138,7 +138,7 @@ object header {
div(form3.cmnToggle("note-mod", "mod", checked = true)),
label(`for` := "note-mod")("For moderators only")
),
isGranted(_.Doxing) option div(
isGranted(_.Admin) option div(
div(form3.cmnToggle("note-dox", "dox", checked = false)),
label(`for` := "note-dox")("Doxing info")
)
@ -153,7 +153,7 @@ object header {
social.notes
.filter { n =>
ctx.me.exists(n.isFrom) ||
isGranted(_.Doxing) ||
isGranted(_.Admin) ||
(!n.dox && isGranted(_.ModNote))
}
.map { note =>

View File

@ -452,6 +452,7 @@ GET /mod/chat-user/:username controllers.Mod.chatUser(username: String
GET /mod/:username/permissions controllers.Mod.permissions(username: String)
POST /mod/:username/permissions controllers.Mod.savePermissions(username: String)
GET /mod/public-chat controllers.Mod.publicChat
POST /mod/public-chat/timeout controllers.Mod.publicChatTimeout
GET /mod/email-confirm controllers.Mod.emailConfirm
GET /mod/chat-panic controllers.Mod.chatPanic
POST /mod/chat-panic controllers.Mod.chatPanicPost

View File

@ -4,18 +4,18 @@ import chess.format.pgn.{ Glyphs, Move, Pgn, Tag, Turn }
import chess.opening._
import chess.{ Color, Status }
import lila.game.GameDrawOffers
import lila.game.Game
final class Annotator(netDomain: lila.common.config.NetDomain) {
def apply(
p: Pgn,
analysis: Option[Analysis],
opening: Option[FullOpening.AtPly],
winner: Option[Color],
status: Status
): Pgn =
annotateStatus(winner, status) {
annotateOpening(opening) {
annotateTurns(p, analysis ?? (_.advices))
def apply(p: Pgn, game: Game, analysis: Option[Analysis]): Pgn =
annotateStatus(game.winnerColor, game.status) {
annotateOpening(game.opening) {
annotateTurns(
annotateDrawOffers(p, game.drawOffers),
analysis.??(_.advices)
)
}.copy(
tags = p.tags + Tag(_.Annotator, netDomain)
)
@ -49,6 +49,19 @@ final class Annotator(netDomain: lila.common.config.NetDomain) {
)
}
private def annotateDrawOffers(pgn: Pgn, drawOffers: GameDrawOffers): Pgn =
if (drawOffers.isEmpty) pgn
else
drawOffers.normalizedPlies.foldLeft(pgn) { case (pgn, ply) =>
pgn.updatePly(
ply,
move => {
val color = !Color.fromPly(ply)
move.copy(comments = s"$color offers draw" :: move.comments)
}
)
}
private def makeVariation(turn: Turn, advice: Advice): List[Turn] =
Turn.fromMoves(
advice.info.variation take 20 map { san =>

View File

@ -35,7 +35,7 @@ final class PgnDump(
else fuccess(pgn)
} map { pgn =>
val evaled = analysis.ifTrue(flags.evals).fold(pgn)(addEvals(pgn, _))
if (flags.literate) annotator(evaled, analysis, game.opening, game.winnerColor, game.status)
if (flags.literate) annotator(evaled, game, analysis)
else evaled
} map { pgn =>
realPlayers.fold(pgn)(_.update(game, pgn))

View File

@ -204,15 +204,7 @@ final private[api] class RoundApi(
obj: JsObject
) =
obj + ("treeParts" -> partitionTreeJsonWriter.writes(
lila.round.TreeBuilder(
id = pov.gameId,
pgnMoves = pov.game.pgnMoves,
variant = pov.game.variant,
analysis = analysis,
initialFen = initialFen | pov.game.variant.initialFen,
withFlags = withFlags,
clocks = withFlags.clocks ?? pov.game.bothClockStates
)
lila.round.TreeBuilder(pov.game, analysis, initialFen | pov.game.variant.initialFen, withFlags)
))
private def withSteps(pov: Pov, initialFen: Option[FEN])(obj: JsObject) =

View File

@ -3,6 +3,7 @@ package lila.appeal
import lila.db.dsl._
import lila.user.User
import org.joda.time.DateTime
import lila.user.Holder
final class AppealApi(
coll: Coll
@ -40,8 +41,8 @@ final class AppealApi(
coll.update.one($id(appeal.id), appeal) inject appeal
}
def reply(text: String, prev: Appeal, mod: User) = {
val appeal = prev.post(text, mod)
def reply(text: String, prev: Appeal, mod: Holder) = {
val appeal = prev.post(text, mod.user)
coll.update.one($id(appeal.id), appeal) inject appeal
}

View File

@ -10,7 +10,7 @@ import lila.common.String.noShouting
import lila.db.dsl._
import lila.hub.actorApi.shutup.{ PublicSource, RecordPrivateChat, RecordPublicChat }
import lila.memo.CacheApi._
import lila.user.{ User, UserRepo }
import lila.user.{ Holder, User, UserRepo }
final class ChatApi(
coll: Coll,
@ -154,6 +154,19 @@ final class ChatApi(
case _ => funit
}
def publicTimeout(data: ChatTimeout.TimeoutFormData, me: Holder): Funit =
ChatTimeout.Reason(data.reason) ?? { reason =>
timeout(
chatId = Chat.Id(data.roomId),
modId = me.id,
userId = data.userId,
reason = reason,
scope = ChatTimeout.Scope.Global,
text = data.text,
busChan = if (data.chan == "tournament") _.Tournament else _.Simul
)
}
def userModInfo(username: String): Fu[Option[UserModInfo]] =
userRepo named username flatMap {
_ ?? { user =>
@ -169,36 +182,42 @@ final class ChatApi(
scope: ChatTimeout.Scope,
text: String,
busChan: BusChan.Select
): Funit = {
val line = c.hasRecentLine(user) option UserLine(
username = systemUserId,
title = None,
text = s"${user.username} was timed out 15 minutes for ${reason.name}.",
troll = false,
deleted = false
)
val c2 = c.markDeleted(user)
val chat = line.fold(c2)(c2.add)
coll.update.one($id(chat.id), chat).void >>
chatTimeout.add(c, mod, user, reason, scope) >>- {
cached invalidate chat.id
publish(chat.id, actorApi.OnTimeout(chat.id, user.id), busChan)
line foreach { l =>
publish(chat.id, actorApi.ChatLine(chat.id, l), busChan)
): Funit =
chatTimeout.add(c, mod, user, reason, scope) flatMap {
_ ?? {
val lineText = scope match {
case ChatTimeout.Scope.Global => s"${user.username} was timed out 15 minutes for ${reason.name}."
case _ => s"${user.username} was timed out 15 minutes by a page mod (not a Lichess mod)"
}
val line = c.hasRecentLine(user) option UserLine(
username = systemUserId,
title = None,
text = lineText,
troll = false,
deleted = false
)
val c2 = c.markDeleted(user)
val chat = line.fold(c2)(c2.add)
coll.update.one($id(chat.id), chat).void >>- {
cached.invalidate(chat.id)
publish(chat.id, actorApi.OnTimeout(chat.id, user.id), busChan)
line foreach { l =>
publish(chat.id, actorApi.ChatLine(chat.id, l), busChan)
}
if (isMod(mod))
lila.common.Bus.publish(
lila.hub.actorApi.mod.ChatTimeout(
mod = mod.id,
user = user.id,
reason = reason.key,
text = text
),
"chatTimeout"
)
else logger.info(s"${mod.username} times out ${user.username} in #${c.id} for ${reason.key}")
}
if (isMod(mod))
lila.common.Bus.publish(
lila.hub.actorApi.mod.ChatTimeout(
mod = mod.id,
user = user.id,
reason = reason.key,
text = text
),
"chatTimeout"
)
else logger.info(s"${mod.username} times out ${user.username} in #${c.id} for ${reason.key}")
}
}
}
def delete(c: UserChat, user: User, busChan: BusChan.Select): Funit = {
val chat = c.markDeleted(user)

View File

@ -1,12 +1,14 @@
package lila.chat
import org.joda.time.DateTime
import play.api.data.Form
import play.api.data.Forms._
import reactivemongo.api.bson._
import scala.concurrent.duration._
import lila.db.dsl._
import lila.user.User
import org.joda.time.DateTime
import reactivemongo.api.bson._
import scala.concurrent.duration._
final class ChatTimeout(
coll: Coll,
duration: FiniteDuration
@ -16,9 +18,9 @@ final class ChatTimeout(
private val global = new lila.memo.ExpireSetMemo(duration)
def add(chat: UserChat, mod: User, user: User, reason: Reason, scope: Scope): Funit =
def add(chat: UserChat, mod: User, user: User, reason: Reason, scope: Scope): Fu[Boolean] =
isActive(chat.id, user.id) flatMap {
case true => funit
case true => fuccess(false)
case false =>
if (scope == Scope.Global) global put user.id
coll.insert
@ -32,8 +34,7 @@ final class ChatTimeout(
"createdAt" -> DateTime.now,
"expiresAt" -> DateTime.now.plusSeconds(duration.toSeconds.toInt)
)
)
.void
) inject true
}
def isActive(chatId: Chat.Id, userId: User.ID): Fu[Boolean] =
@ -66,7 +67,9 @@ final class ChatTimeout(
object ChatTimeout {
sealed abstract class Reason(val key: String, val name: String)
sealed abstract class Reason(val key: String, val name: String) {
lazy val shortName = name.split(';').lift(0) | name
}
object Reason {
case object PublicShaming extends Reason("shaming", "public shaming; please use lichess.org/report")
@ -93,4 +96,16 @@ object ChatTimeout {
case object Local extends Scope
case object Global extends Scope
}
val form = Form(
mapping(
"roomId" -> nonEmptyText,
"chan" -> lila.common.Form.stringIn(Set("tournament", "simul")),
"userId" -> nonEmptyText,
"reason" -> nonEmptyText,
"text" -> nonEmptyText
)(TimeoutFormData.apply)(TimeoutFormData.unapply)
)
case class TimeoutFormData(roomId: String, chan: String, userId: User.ID, reason: String, text: String)
}

View File

@ -12,6 +12,7 @@ sealed trait Line {
def isHuman = !isSystem
def humanAuthor = isHuman option author
def troll: Boolean
def userIdMaybe: Option[User.ID]
}
case class UserLine(
@ -26,17 +27,22 @@ case class UserLine(
def userId = User normalize username
def userIdMaybe = userId.some
def delete = copy(deleted = true)
def isVisible = !troll && !deleted
def isLichess = userId == User.lichessId
}
case class PlayerLine(
color: Color,
text: String
) extends Line {
def deleted = false
def author = color.name
def troll = false
def deleted = false
def author = color.name
def troll = false
def userIdMaybe = none
}
object Line {

View File

@ -10,6 +10,7 @@ import lila.db.dsl._
import lila.msg.MsgApi
import lila.security.Permission
import lila.user.{ Authenticator, User, UserRepo }
import lila.user.Holder
final class ClasApi(
colls: ClasColls,
@ -250,12 +251,12 @@ final class ClasApi(
authenticator.setPassword(s.userId, password) inject password
}
def archive(sId: Student.Id, t: User, v: Boolean): Fu[Option[Student]] =
def archive(sId: Student.Id, by: Holder, v: Boolean): Fu[Option[Student]] =
coll.ext
.findAndUpdate[Student](
selector = $id(sId),
update =
if (v) $set("archived" -> Clas.Recorded(t.id, DateTime.now))
if (v) $set("archived" -> Clas.Recorded(by.id, DateTime.now))
else $unset("archived"),
fetchNewObject = true
)
@ -280,9 +281,9 @@ ${clas.desc}""",
import ClasInvite.Feedback._
def create(clas: Clas, user: User, realName: String, teacher: User): Fu[ClasInvite.Feedback] =
def create(clas: Clas, user: User, realName: String, teacher: Holder): Fu[ClasInvite.Feedback] =
student
.archive(Student.id(user.id, clas.id), user, v = false)
.archive(Student.id(user.id, clas.id), teacher, v = false)
.map2[ClasInvite.Feedback](_ => Already) getOrElse {
lila.mon.clas.student.invite(teacher.id).increment()
val invite = ClasInvite.make(clas, user, realName, teacher)
@ -342,7 +343,7 @@ ${clas.desc}""",
colls.invite.delete.one($id(id)).void
private def sendInviteMessage(
teacher: User,
teacher: Holder,
student: User,
clas: Clas,
invite: ClasInvite

View File

@ -2,7 +2,7 @@ package lila.clas
import org.joda.time.DateTime
import lila.user.User
import lila.user.{ Holder, User }
case class ClasInvite(
_id: ClasInvite.Id, // random
@ -17,7 +17,7 @@ object ClasInvite {
case class Id(value: String) extends AnyVal with StringValue
def make(clas: Clas, user: User, realName: String, teacher: User) =
def make(clas: Clas, user: User, realName: String, teacher: Holder) =
ClasInvite(
_id = Id(lila.common.ThreadLocalRandom nextString 8),
userId = user.id,

View File

@ -7,7 +7,7 @@ import lila.db.dsl._
import lila.db.Photographer
import lila.notify.{ Notification, NotifyApi }
import lila.security.Granter
import lila.user.{ User, UserRepo }
import lila.user.{ Holder, User, UserRepo }
final class CoachApi(
coachColl: Coll,
@ -32,10 +32,10 @@ final class CoachApi(
}
}
def findOrInit(user: User): Fu[Option[Coach.WithUser]] =
Granter(_.Coach)(user) ?? {
find(user) orElse {
val c = Coach.WithUser(Coach make user, user)
def findOrInit(coach: Holder): Fu[Option[Coach.WithUser]] =
Granter.is(_.Coach)(coach) ?? {
find(coach.user) orElse {
val c = Coach.WithUser(Coach make coach.user, coach.user)
coachColl.insert.one(c.coach) inject c.some
}
}

View File

@ -34,9 +34,7 @@ trait dsl {
def $doc(elements: Iterable[(String, BSONValue)]): Bdoc = BSONDocument.strict(elements)
def $arr(elements: Producer[BSONValue]*): Barr = {
BSONArray(elements: _*)
}
def $arr(elements: Producer[BSONValue]*): Barr = BSONArray(elements: _*)
def $id[T: BSONWriter](id: T): Bdoc = $doc("_id" -> id)

View File

@ -24,7 +24,7 @@ case class PlayerAggregateAssessment(
def scoreLikelyCheatingGames(x: Double) =
(weightedCheatingSum + weightedLikelyCheatingSum) / assessmentsCount >= (x / 100)
val markable: Boolean = !isGreatUser && isWorthLookingAt &&
val markable: Boolean = !user.hasTitle && !isGreatUser && isWorthLookingAt &&
(weightedCheatingSum >= 3 || weightedCheatingSum + weightedLikelyCheatingSum >= 6) &&
(scoreCheatingGames(8) || scoreLikelyCheatingGames(16))

View File

@ -60,17 +60,19 @@ object PlayerAssessment {
val basics = makeBasics(pov, holdAlerts)
def blursMatter = !game.isSimul && game.hasClock
lazy val highBlurRate: Boolean =
!game.isSimul && game.playerBlurPercent(color) > 90
blursMatter && game.playerBlurPercent(color) > 90
lazy val moderateBlurRate: Boolean =
!game.isSimul && game.playerBlurPercent(color) > 70
blursMatter && game.playerBlurPercent(color) > 70
val highestChunkBlurs = highestChunkBlursOf(pov)
val highChunkBlurRate: Boolean = highestChunkBlurs >= 11
val highChunkBlurRate: Boolean = blursMatter && highestChunkBlurs >= 11
val moderateChunkBlurRate: Boolean = highestChunkBlurs >= 8
val moderateChunkBlurRate: Boolean = blursMatter && highestChunkBlurs >= 8
lazy val highlyConsistentMoveTimes: Boolean =
game.clock.exists(_.estimateTotalSeconds > 60) && {

View File

@ -8,7 +8,7 @@ import lila.db.dsl._
import lila.db.paginator._
import lila.hub.actorApi.timeline.{ ForumPost, Propagate }
import lila.security.{ Granter => MasterGranter }
import lila.user.User
import lila.user.{ Holder, User }
final private[forum] class TopicApi(
env: Env,
@ -159,23 +159,23 @@ final private[forum] class TopicApi(
env.recent.invalidate()
}
def toggleClose(categ: Categ, topic: Topic, mod: User): Funit =
def toggleClose(categ: Categ, topic: Topic, mod: Holder): Funit =
env.topicRepo.close(topic.id, topic.open) >> {
MasterGranter(_.ModerateForum)(mod) ??
MasterGranter.is(_.ModerateForum)(mod) ??
modLog.toggleCloseTopic(mod.id, categ.name, topic.name, topic.open)
}
def toggleHide(categ: Categ, topic: Topic, mod: User): Funit =
def toggleHide(categ: Categ, topic: Topic, mod: Holder): Funit =
env.topicRepo.hide(topic.id, topic.visibleOnHome) >> {
MasterGranter(_.ModerateForum)(mod) ?? {
MasterGranter.is(_.ModerateForum)(mod) ?? {
env.postRepo.hideByTopic(topic.id, topic.visibleOnHome) >>
modLog.toggleHideTopic(mod.id, categ.name, topic.name, topic.visibleOnHome)
} >>- env.recent.invalidate()
}
def toggleSticky(categ: Categ, topic: Topic, mod: User): Funit =
def toggleSticky(categ: Categ, topic: Topic, mod: Holder): Funit =
env.topicRepo.sticky(topic.id, !topic.isSticky) >> {
MasterGranter(_.ModerateForum)(mod) ??
MasterGranter.is(_.ModerateForum)(mod) ??
modLog.toggleStickyTopic(mod.id, categ.name, topic.name, !topic.isSticky)
}

View File

@ -66,6 +66,18 @@ object BSONHandlers {
)
}
implicit private[game] val gameDrawOffersHandler = tryHandler[GameDrawOffers](
{ case arr: BSONArray =>
Success(arr.values.foldLeft(GameDrawOffers.empty) {
case (offers, BSONInteger(p)) =>
if (p > 0) offers.copy(white = offers.white incl p)
else offers.copy(black = offers.black incl -p)
case (offers, _) => offers
})
},
offers => BSONArray((offers.white ++ offers.black.map(-_)).view.map(BSONInteger.apply).toIndexedSeq)
)
import Player.playerBSONHandler
private val emptyPlayerBuilder = playerBSONHandler.read($empty)
@ -160,7 +172,8 @@ object BSONHandlers {
tournamentId = r strO F.tournamentId,
swissId = r strO F.swissId,
simulId = r strO F.simulId,
analysed = r boolD F.analysed
analysed = r boolD F.analysed,
drawOffers = r.getD(F.drawOffers, GameDrawOffers.empty)
)
)
}

View File

@ -317,9 +317,8 @@ object Event {
}
case class DrawOffer(by: Option[Color]) extends Event {
def typ = "reload"
def data = reloadOr("drawOffer", by)
override def owner = true
def typ = "reload"
def data = reloadOr("drawOffer", by)
}
case class ClockInc(color: Color, time: Centis) extends Event {

View File

@ -315,15 +315,21 @@ case class Game(
blackPlayer = f(blackPlayer)
)
def drawOffers = metadata.drawOffers
def playerCanOfferDraw(color: Color) =
started && playable &&
turns >= 2 &&
!player(color).isOfferingDraw &&
!opponent(color).isAi &&
!playerHasOfferedDraw(color)
!playerHasOfferedDrawRecently(color)
def playerHasOfferedDraw(color: Color) =
player(color).lastDrawOffer ?? (_ >= turns - 20)
def playerHasOfferedDrawRecently(color: Color) =
drawOffers.lastBy(color) ?? (_ >= turns - 20)
def offerDraw(color: Color) = copy(
metadata = metadata.copy(drawOffers = drawOffers.add(color, turns))
).updatePlayer(color, _.offerDraw)
def playerCouldRematch =
finishedOrAborted &&
@ -735,14 +741,7 @@ object Game {
status = Status.Created,
daysPerTurn = daysPerTurn,
mode = mode,
metadata = Metadata(
source = source.some,
pgnImport = pgnImport,
tournamentId = none,
swissId = none,
simulId = none,
analysed = false
),
metadata = metadata(source).copy(pgnImport = pgnImport),
createdAt = createdAt,
movedAt = createdAt
)
@ -756,7 +755,8 @@ object Game {
tournamentId = none,
swissId = none,
simulId = none,
analysed = false
analysed = false,
drawOffers = GameDrawOffers.empty
)
object BSONFields {
@ -800,6 +800,7 @@ object Game {
val initialFen = "if"
val checkAt = "ck"
val perfType = "pt" // only set on student games for aggregation
val drawOffers = "do"
}
}

View File

@ -96,11 +96,11 @@ object GameDiff {
BSONHandlers.clockBSONWrite(a.createdAt, c).toOption
}
)
dTry(drawOffers, _.drawOffers, BSONHandlers.gameDrawOffersHandler.writeTry)
for (i <- 0 to 1) {
import Player.BSONFields._
val name = s"p$i."
val player: Game => Player = if (i == 0) (_.whitePlayer) else (_.blackPlayer)
dOpt(s"$name$lastDrawOffer", player(_).lastDrawOffer, (l: Option[Int]) => l flatMap w.intO)
dOpt(s"$name$isOfferingDraw", player(_).isOfferingDraw, w.boolO)
dOpt(s"$name$proposeTakebackAt", player(_).proposeTakebackAt, w.intO)
dTry(s"$name$blursBits", player(_).blurs, Blurs.BlursBSONHandler.writeTry)

View File

@ -327,8 +327,6 @@ final class GameRepo(val coll: Coll)(implicit ec: scala.concurrent.ExecutionCont
F.positionHashes -> true,
F.playingUids -> true,
F.unmovedRooks -> true,
("p0." + Player.BSONFields.lastDrawOffer) -> true,
("p1." + Player.BSONFields.lastDrawOffer) -> true,
("p0." + Player.BSONFields.isOfferingDraw) -> true,
("p1." + Player.BSONFields.isOfferingDraw) -> true,
("p0." + Player.BSONFields.proposeTakebackAt) -> true,

View File

@ -35,6 +35,7 @@ final class JsonView(rematches: Rematches) {
.add("lastMove" -> game.lastMoveKeys)
.add("check" -> game.situation.checkSquare.map(_.key))
.add("rematch" -> rematches.of(game.id))
.add("drawOffers" -> (!game.drawOffers.isEmpty).option(game.drawOffers.normalizedPlies))
}
object JsonView {

View File

@ -2,6 +2,7 @@ package lila.game
import java.security.MessageDigest
import lila.db.ByteArray
import chess.Color
private[game] case class Metadata(
source: Option[Source],
@ -9,7 +10,8 @@ private[game] case class Metadata(
tournamentId: Option[String],
swissId: Option[String],
simulId: Option[String],
analysed: Boolean
analysed: Boolean,
drawOffers: GameDrawOffers
) {
def pgnDate = pgnImport flatMap (_.date)
@ -21,7 +23,30 @@ private[game] case class Metadata(
private[game] object Metadata {
val empty = Metadata(None, None, None, None, None, analysed = false)
val empty = Metadata(None, None, None, None, None, analysed = false, GameDrawOffers.empty)
}
// plies
case class GameDrawOffers(white: Set[Int], black: Set[Int]) {
def lastBy(color: Color): Option[Int] = color.fold(white, black).maxOption
def add(color: Color, ply: Int) =
color.fold(copy(white = white incl ply), copy(black = black incl ply))
def isEmpty = this == GameDrawOffers.empty
// lichess allows to offer draw on either turn,
// normalize to pretend it was done on the opponent turn.
def normalize(color: Color): Set[Int] = color.fold(white, black) map {
case ply if (ply % 2 == 0) == color.white => ply + 1
case ply => ply
}
def normalizedPlies: Set[Int] = normalize(chess.White) ++ normalize(chess.Black)
}
object GameDrawOffers {
val empty = GameDrawOffers(Set.empty, Set.empty)
}
case class PgnImport(

View File

@ -14,7 +14,6 @@ case class Player(
aiLevel: Option[Int],
isWinner: Option[Boolean] = None,
isOfferingDraw: Boolean = false,
lastDrawOffer: Option[Int] = None,
proposeTakebackAt: Int = 0, // ply when takeback was proposed
userId: Player.UserId = None,
rating: Option[Int] = None,
@ -49,11 +48,7 @@ case class Player(
def finish(winner: Boolean) = copy(isWinner = winner option true)
def offerDraw(turn: Int) =
copy(
isOfferingDraw = true,
lastDrawOffer = Some(turn)
)
def offerDraw = copy(isOfferingDraw = true)
def removeDrawOffer = copy(isOfferingDraw = false)
@ -165,7 +160,6 @@ object Player {
val aiLevel = "ai"
val isOfferingDraw = "od"
val lastDrawOffer = "ld"
val proposeTakebackAt = "ta"
val rating = "e"
val ratingDiff = "d"
@ -206,7 +200,6 @@ object Player {
aiLevel = r intO aiLevel,
isWinner = win,
isOfferingDraw = r boolD isOfferingDraw,
lastDrawOffer = r intO lastDrawOffer,
proposeTakebackAt = r intD proposeTakebackAt,
userId = userId,
rating = r intO rating flatMap ratingRange,
@ -222,7 +215,6 @@ object Player {
BSONDocument(
aiLevel -> p.aiLevel,
isOfferingDraw -> w.boolO(p.isOfferingDraw),
lastDrawOffer -> p.lastDrawOffer,
proposeTakebackAt -> w.intO(p.proposeTakebackAt),
rating -> p.rating,
ratingDiff -> p.ratingDiff,

View File

@ -35,6 +35,8 @@ case class Pov(game: Game, color: Color) {
def hasMoved = game playerHasMoved color
def moves = game playerMoves color
def win = game wonBy color
def loss = game lostBy color

View File

@ -356,6 +356,8 @@ val `latestUpdates` = new I18nKey("latestUpdates")
val `tournamentWinners` = new I18nKey("tournamentWinners")
val `name` = new I18nKey("name")
val `description` = new I18nKey("description")
val `descPrivate` = new I18nKey("descPrivate")
val `descPrivateHelp` = new I18nKey("descPrivateHelp")
val `no` = new I18nKey("no")
val `yes` = new I18nKey("yes")
val `help` = new I18nKey("help")

View File

@ -5,6 +5,7 @@ import org.joda.time.DateTime
import lila.common.{ ApiVersion, EmailAddress, Heapsort, IpAddress, LightUser }
import lila.hub.actorApi.slack._
import lila.user.User
import lila.user.Holder
final class SlackApi(
client: SlackClient,
@ -22,16 +23,9 @@ final class SlackApi(
implicit private val amountOrdering = Ordering.by[ChargeEvent, Int](_.amount)
def apply(event: ChargeEvent): Funit =
if (event.amount < 10000) addToBuffer(event)
else
displayMessage {
s"${userAt(event.username)} donated ${amount(event.amount)}. Monthly progress: ${event.percent}%"
}
private def addToBuffer(event: ChargeEvent): Funit = {
def apply(event: ChargeEvent): Funit = {
buffer = buffer :+ event
(buffer.head.date isBefore DateTime.now.minusHours(12)) ?? {
buffer.head.date.isBefore(DateTime.now.minusHours(12)) ?? {
val firsts = Heapsort.topN(buffer, 10, amountOrdering).map(_.username).map(userAt).mkString(", ")
val amountSum = buffer.map(_.amount).sum
val patrons =
@ -80,10 +74,10 @@ final class SlackApi(
)
}
def commlog(mod: User, user: User, reportBy: Option[User.ID]): Funit =
def commlog(mod: Holder, user: User, reportBy: Option[User.ID]): Funit =
client(
SlackMessage(
username = mod.username,
username = mod.user.username,
icon = "eye",
text = {
val finalS = if (user.username endsWith "s") "" else "s"
@ -123,10 +117,10 @@ final class SlackApi(
}
}
def chatPanic(mod: User, v: Boolean): Funit =
def chatPanic(mod: Holder, v: Boolean): Funit =
client(
SlackMessage(
username = mod.username,
username = mod.user.username,
icon = if (v) "anger" else "information_source",
text = s"${if (v) "Enabled" else "Disabled"} $chatPanicLink",
channel = rooms.tavern
@ -218,21 +212,21 @@ final class SlackApi(
private def linkifyUsers(msg: String) =
userRegex matcher msg replaceAll userReplace
def userMod(user: User, mod: User): Funit =
def userMod(user: User, mod: Holder): Funit =
noteApi
.forMod(user.id)
.map(_.headOption.filter(_.date isAfter DateTime.now.minusMinutes(5)))
.map {
case None =>
SlackMessage(
username = mod.username,
username = mod.user.username,
icon = "eyes",
text = s"Let's have a look at _*${userLink(user.username)}*_",
channel = rooms.tavern
)
case Some(note) =>
SlackMessage(
username = mod.username,
username = mod.user.username,
icon = "spiral_note_pad",
text = s"_*${userLink(user.username)}*_ (${userNotesLink(user.username)}):\n" +
linkifyUsers(note.text take 2000),
@ -251,10 +245,10 @@ final class SlackApi(
)
)
def userAppeal(user: User, mod: User): Funit =
def userAppeal(user: User, mod: Holder): Funit =
client(
SlackMessage(
username = mod.username,
username = mod.user.username,
icon = "eyes",
text =
s"Let's have a look at the appeal of _*${lichessLink(s"/appeal/${user.username}", user.username)}*_",

View File

@ -4,6 +4,7 @@ import org.joda.time.DateTime
import reactivemongo.api.bson._
import reactivemongo.api.ReadPreference
import lila.analyse.Analysis
import lila.analyse.Analysis.Analyzed
import lila.analyse.AnalysisRepo
import lila.common.Bus
@ -11,8 +12,7 @@ import lila.db.dsl._
import lila.game.{ Game, GameRepo, Pov, Query }
import lila.report.{ Mod, ModId, Report, Reporter, Suspect, SuspectId }
import lila.tournament.{ Tournament, TournamentTop }
import lila.user.{ User, UserRepo }
import lila.analyse.Analysis
import lila.user.{ Holder, User, UserRepo }
final class IrwinApi(
reportColl: Coll,
@ -86,8 +86,8 @@ final class IrwinApi(
import IrwinRequest.Origin
def fromMod(suspect: Suspect, mod: Mod) = {
notification.add(suspect.id, mod.id)
def fromMod(suspect: Suspect, mod: Holder) = {
notification.add(suspect.id, ModId(mod.id))
insert(suspect, _.Moderator)
}

View File

@ -153,7 +153,7 @@ final class AssessApi(
def assessUser(userId: User.ID): Funit =
getPlayerAggregateAssessment(userId) flatMap {
case Some(playerAggregateAssessment) =>
_ ?? { playerAggregateAssessment =>
playerAggregateAssessment.action match {
case AccountAction.Engine | AccountAction.EngineAndBan =>
userRepo.getTitle(userId).flatMap {
@ -171,7 +171,7 @@ final class AssessApi(
// reporter ! lila.hub.actorApi.report.Clean(userId)
funit
}
case _ => funit
}
}
private val assessableSources: Set[Source] =
@ -219,6 +219,8 @@ final class AssessApi(
val shouldAnalyse: Fu[Option[AutoAnalysis.Reason]] =
if (!game.analysable) fuccess(none)
else if (game.speed >= chess.Speed.Blitz && (white.hasTitle || black.hasTitle))
fuccess(TitledPlayer.some)
else if (!game.source.exists(assessableSources.contains)) fuccess(none)
// give up on correspondence games
else if (game.isCorrespondence) fuccess(none)

View File

@ -13,5 +13,6 @@ object AutoAnalysis {
case object Blurs extends Reason
case object WinnerRatingProgress extends Reason
case object NewPlayerWin extends Reason
case object TitledPlayer extends Reason
}
}

View File

@ -78,6 +78,8 @@ final class Env(
lazy val presets = wire[ModPresetsApi]
lazy val ipRender = wire[IpRender]
private lazy val sandbagWatch = wire[SandbagWatch]
lila.common.Bus.subscribeFuns(
@ -91,8 +93,11 @@ final class Env(
if (game.status == chess.Status.Cheat)
game.loserUserId foreach { userId =>
logApi.cheatDetected(userId, game.id) >>
logApi.countRecentCheatDetected(userId) flatMap {
reportApi.autoCheatDetectedReport(userId, _)
logApi.countRecentCheatDetected(userId) flatMap { count =>
(count >= 3) ?? {
if (game.hasClock) api.autoMark(lila.report.SuspectId(userId), lila.report.ModId.lichess)
else reportApi.autoCheatDetectedReport(userId, count)
}
}
}
},

View File

@ -0,0 +1,43 @@
package lila.mod
import com.github.blemale.scaffeine.Cache
import com.github.blemale.scaffeine.LoadingCache
import scala.concurrent.duration._
import scala.jdk.CollectionConverters._
import lila.common.CuteNameGenerator
import lila.common.IpAddress
import lila.memo.CacheApi
import lila.security.Granter
import lila.user.Holder
import lila.common.ThreadLocalRandom
object IpRender {
type Raw = String
type Rendered = String
type RenderIp = IpAddress => Rendered
}
final class IpRender {
import IpRender._
def apply(mod: Holder): RenderIp = if (Granter.is(_.Admin)(mod)) visible else encrypted
val visible = (ip: IpAddress) => ip.value
val encrypted = (ip: IpAddress) => cache get ip
def decrypt(str: String): Option[IpAddress] = IpAddress.from(str) orElse
cache.underlying.asMap.asScala.collectFirst {
case (ip, encrypted) if encrypted == str =>
ip
}
private val cache: LoadingCache[IpAddress, Rendered] = CacheApi.scaffeineNoScheduler
.expireAfterAccess(30 minutes)
.build((_: IpAddress) =>
s"NoIP:${~CuteNameGenerator.make(maxSize = 30)}-${ThreadLocalRandom.nextString(3)}"
)
}

View File

@ -3,7 +3,7 @@ package lila.mod
import lila.common.{ Bus, EmailAddress }
import lila.report.{ Mod, ModId, Room, Suspect, SuspectId }
import lila.security.{ Granter, Permission }
import lila.user.{ LightUserApi, Title, User, UserRepo }
import lila.user.{ Holder, LightUserApi, Title, User, UserRepo }
final class ModApi(
userRepo: UserRepo,
@ -133,14 +133,14 @@ final class ModApi(
logApi.setEmail(mod, user.id)
}
def setPermissions(mod: Mod, username: String, permissions: Set[Permission]): Funit =
def setPermissions(mod: Holder, username: String, permissions: Set[Permission]): Funit =
withUser(username) { user =>
val finalPermissions = Permission(user.roles).filter { p =>
// only remove permissions the mod can actually grant
permissions.contains(p) || !Granter.canGrant(mod.user, p)
permissions.contains(p) || !Granter.canGrant(mod, p)
} ++
// only add permissions the mod can actually grant
permissions.filter(Granter.canGrant(mod.user, _))
permissions.filter(Granter.canGrant(mod, _))
userRepo.setRoles(user.id, finalPermissions.map(_.dbKey).toList) >> {
Bus.publish(
lila.hub.actorApi.mod.SetPermissions(user.id, finalPermissions.map(_.dbKey).toList),

View File

@ -5,7 +5,7 @@ import org.joda.time.DateTime
import lila.db.dsl._
import lila.report.{ Mod, ModId, Report, Suspect }
import lila.security.Permission
import lila.user.{ User, UserRepo }
import lila.user.{ Holder, User, UserRepo }
final class ModlogApi(repo: ModlogRepo, userRepo: UserRepo, slackApi: lila.irc.SlackApi)(implicit
ec: scala.concurrent.ExecutionContext
@ -170,10 +170,10 @@ final class ModlogApi(repo: ModlogRepo, userRepo: UserRepo, slackApi: lila.irc.S
Modlog(mod, user.some, Modlog.chatTimeout, details = s"$reason: $text".some)
}
def setPermissions(mod: Mod, user: User.ID, permissions: Map[Permission, Boolean]) =
def setPermissions(mod: Holder, user: User.ID, permissions: Map[Permission, Boolean]) =
add {
Modlog(
mod.id.value,
mod.id,
user.some,
Modlog.permissions,
details = permissions

View File

@ -36,16 +36,15 @@ final private class SandbagWatch(
}
else {
records.put(userId, record)
if (record.latest has Sandbag) {
if (record.count(Sandbag) == 3) sendMessage(userId, MsgPreset.sandbagAuto)
else if (record.count(Sandbag) == 4) withWinnerAndLoser(game)(reportApi.autoSandbagReport)
else funit
} else {
val boostCount = record.samePlayerBoostCount
if (boostCount == 3) sendMessage(userId, MsgPreset.boostAuto)
else if (boostCount == 4) withWinnerAndLoser(game)(reportApi.autoBoostReport)
else funit
val sandbagCount = record.countSandbagWithLatest
val boostCount = record.samePlayerBoostCount
if (sandbagCount == 3) sendMessage(userId, MsgPreset.sandbagAuto)
else if (sandbagCount == 4) game.loserUserId ?? { loser =>
reportApi.autoSandbagReport(record.sandbagOpponents, loser)
}
else if (boostCount == 3) sendMessage(userId, MsgPreset.boostAuto)
else if (boostCount == 4) withWinnerAndLoser(game)(reportApi.autoBoostReport)
else funit
}
private def sendMessage(userId: User.ID, preset: MsgPreset): Funit =
@ -70,7 +69,8 @@ final private class SandbagWatch(
.playerByUserId(userId)
.ifTrue(isSandbag(game))
.fold[Outcome](Good) { player =>
if (player.color == loser) Sandbag else game.loserUserId.fold[Outcome](Good)(Boost.apply)
if (player.color == loser) game.winnerUserId.fold[Outcome](Good)(Sandbag.apply)
else game.loserUserId.fold[Outcome](Good)(Boost.apply)
}
private def isSandbag(game: Game): Boolean =
@ -83,9 +83,9 @@ final private class SandbagWatch(
private object SandbagWatch {
sealed trait Outcome
case object Good extends Outcome
case object Sandbag extends Outcome
case class Boost(opponent: User.ID) extends Outcome
case object Good extends Outcome
case class Sandbag(opponent: User.ID) extends Outcome
case class Boost(opponent: User.ID) extends Outcome
val maxOutcomes = 7
@ -99,6 +99,20 @@ private object SandbagWatch {
def immaculate = outcomes.sizeIs == maxOutcomes && outcomes.forall(Good ==)
def latestIsSandbag = latest exists {
case Sandbag(_) => true
case _ => false
}
def countSandbagWithLatest: Int = latestIsSandbag ?? outcomes.count {
case Sandbag(_) => true
case _ => false
}
def sandbagOpponents = outcomes.collect { case Sandbag(opponent) =>
opponent
}.distinct
def samePlayerBoostCount = latest ?? {
case Boost(opponent) =>
outcomes.count {

View File

@ -11,6 +11,7 @@ import lila.common.LilaStream
import lila.common.{ Bus, LightUser }
import lila.db.dsl._
import lila.user.{ User, UserRepo }
import lila.user.Holder
final class MsgApi(
colls: MsgColls,
@ -154,7 +155,7 @@ final class MsgApi(
def systemPost(destId: User.ID, text: String) =
post(User.lichessId, destId, text, multi = true)
def multiPost(orig: User, destSource: Source[User.ID, _], text: String): Fu[Int] =
def multiPost(orig: Holder, destSource: Source[User.ID, _], text: String): Fu[Int] =
destSource
.filter(orig.id !=)
.mapAsync(4) {
@ -166,7 +167,7 @@ final class MsgApi(
def cliMultiPost(orig: String, dests: Seq[User.ID], text: String): Fu[String] =
userRepo named orig flatMap {
case None => fuccess(s"Unknown sender $orig")
case Some(sender) => multiPost(sender, Source(dests), text) inject "done"
case Some(sender) => multiPost(Holder(sender), Source(dests), text) inject "done"
}
def recentByForMod(user: User, nb: Int): Fu[List[MsgConvo]] =

View File

@ -52,8 +52,8 @@ final private[puzzle] class DailyPuzzle(
.path {
_.aggregateOne() { framework =>
import framework._
Match(pathApi.select(PuzzleTheme.mix.key, PuzzleTier.Top, 1900 to 2100)) -> List(
Sample(2),
Match(pathApi.select(PuzzleTheme.mix.key, PuzzleTier.Top, 2000 to 2100)) -> List(
Sample(3),
Project($doc("ids" -> true, "_id" -> false)),
UnwindField("ids"),
PipelineOperator(

View File

@ -1,14 +1,14 @@
package lila.report
import lila.user.User
import lila.user.{ Holder, User }
final class ModReportFilter {
// mutable storage, because I cba to put it in DB
private var modIdFilter = Map.empty[User.ID, Option[Room]]
def get(mod: User): Option[Room] = modIdFilter.get(mod.id).flatten
def get(mod: Holder): Option[Room] = modIdFilter.get(mod.id).flatten
def set(mod: User, filter: Option[Room]) =
def set(mod: Holder, filter: Option[Room]) =
modIdFilter = modIdFilter + (mod.id -> filter)
}

View File

@ -1,6 +1,6 @@
package lila.report
import lila.user.User
import lila.user.{ Holder, User }
sealed trait Reason {
@ -12,7 +12,10 @@ sealed trait Reason {
object Reason {
case object Cheat extends Reason
case object CheatPrint extends Reason {
case object CheatPrint extends Reason { // BC, replaced with AltPrint
override def name = "Print"
}
case object AltPrint extends Reason {
override def name = "Print"
}
case object Comm extends Reason {
@ -22,9 +25,7 @@ object Reason {
case object Other extends Reason
case object Playbans extends Reason
// val communication: Set[Reason] = Set(Insult, Troll, CommFlag, Other)
val all = List(Cheat, CheatPrint, Comm, Boost, Other)
val all = List(Cheat, AltPrint, Comm, Boost, Other, CheatPrint)
val keys = all map (_.key)
val byKey = all map { v =>
(v.key, v)
@ -39,18 +40,19 @@ object Reason {
def isCheat = reason == Cheat
def isOther = reason == Other
def isPrint = reason == CheatPrint
def isPrint = reason == AltPrint || reason == CheatPrint
def isComm = reason == Comm
def isBoost = reason == Boost
def isPlaybans = reason == Playbans
}
def isGrantedFor(mod: User)(reason: Reason) = {
def isGrantedFor(mod: Holder)(reason: Reason) = {
import lila.security.Granter
reason match {
case Cheat => Granter(_.MarkEngine)(mod)
case CheatPrint => Granter(_.ViewIpPrint)(mod)
case Comm => Granter(_.Shadowban)(mod)
case Boost | Playbans | Other => Granter(_.MarkBooster)(mod)
case Cheat => Granter.is(_.MarkEngine)(mod)
case AltPrint | CheatPrint => Granter.is(_.Admin)(mod)
case Comm => Granter.is(_.Shadowban)(mod)
case Boost | Playbans | Other => Granter.is(_.MarkBooster)(mod)
}
}
}

View File

@ -128,6 +128,7 @@ object Report {
def scored(score: Score) = Candidate.Scored(this, score)
def isAutomatic = reporter.id == ReporterId.lichess
def isAutoComm = isAutomatic && isComm
def isAutoBoost = isAutomatic && isBoost
def isCoachReview = isOther && text.contains("COACH REVIEW")
def isCommFlag = text contains Reason.Comm.flagText
}

View File

@ -121,11 +121,11 @@ final class ReportApi(
def getSuspect(username: String): Fu[Option[Suspect]] =
userRepo named username dmap2 Suspect.apply
def autoCheatPrintReport(userId: String): Funit =
def autoAltPrintReport(userId: String): Funit =
coll.exists(
$doc(
"user" -> userId,
"reason" -> Reason.CheatPrint.key
"reason" -> Reason.AltPrint.key
)
) flatMap {
case true => funit // only report once
@ -136,8 +136,8 @@ final class ReportApi(
Candidate(
reporter = reporter,
suspect = suspect,
reason = Reason.CheatPrint,
text = "Shares print with known cheaters"
reason = Reason.AltPrint,
text = "Shares print with suspicious accounts"
)
)
case _ => funit
@ -163,14 +163,14 @@ final class ReportApi(
def autoCheatDetectedReport(userId: User.ID, cheatedGames: Int): Funit =
userRepo.byId(userId) zip getLichessReporter flatMap {
case Some(user) ~ reporter if !user.lame && cheatedGames >= 3 =>
case Some(user) ~ reporter if !user.marks.engine =>
lila.mon.cheat.autoReport.increment()
create(
Candidate(
reporter = reporter,
suspect = Suspect(user),
reason = Reason.Cheat,
text = s"$cheatedGames cheat detected in the last 6 months"
text = s"$cheatedGames cheat detected in the last 6 months; last one is correspondence"
)
)
case _ => funit
@ -251,15 +251,15 @@ final class ReportApi(
case _ => funit
}
def autoSandbagReport(winnerId: User.ID, loserId: User.ID): Funit =
userRepo.pair(winnerId, loserId) zip getLichessReporter flatMap {
case Some((winner, loser)) ~ reporter if !winner.lame && !loser.lame =>
def autoSandbagReport(winnerIds: List[User.ID], loserId: User.ID): Funit =
userRepo.byId(loserId) zip getLichessReporter flatMap {
case Some(loser) ~ reporter if !loser.lame =>
create(
Candidate(
reporter = reporter,
suspect = Suspect(loser),
reason = Reason.Boost,
text = s"Sandbagging: throws games to @${winner.username}"
text = s"Sandbagging: throws games to ${winnerIds.map("@" + _) mkString " "}"
)
)
case _ => funit

View File

@ -12,14 +12,14 @@ final private class ReportScore(
impl.autoScore(candidate)
} map
impl.fixedAutoCommPrintScore(candidate) map
impl.fixedBoostScore(candidate) map
impl.commFlagScore(candidate) map { score =>
candidate scored Report.Score(score atLeast 5 atMost 100)
}
private object impl {
val baseScore = 20
val baseScoreAboveThreshold = 50
val baseScore = 20
def accuracyScore(a: Option[Accuracy]): Double =
a ?? { accuracy =>
@ -34,7 +34,11 @@ final private class ReportScore(
// https://github.com/ornicar/lila/issues/4587
def fixedAutoCommPrintScore(c: Report.Candidate)(score: Double): Double =
if (c.isAutoComm) baseScore
else if (c.isPrint || c.isCoachReview || c.isPlaybans) baseScoreAboveThreshold
else if (c.isPrint || c.isCoachReview || c.isPlaybans) baseScore * 2
else score
def fixedBoostScore(c: Report.Candidate)(score: Double): Double =
if (c.isAutoBoost) baseScore
else score
def commFlagScore(c: Report.Candidate)(score: Double): Double =

View File

@ -1,6 +1,6 @@
package lila.report
import lila.user.User
import lila.user.{ Holder, User }
sealed trait Room {
@ -33,7 +33,7 @@ object Room {
def apply(reason: Reason): Room =
reason match {
case Reason.Cheat => Cheat
case Reason.CheatPrint => Print
case Reason.AltPrint | Reason.CheatPrint => Print
case Reason.Comm => Comm
case Reason.Boost | Reason.Playbans | Reason.Other => Other
}
@ -41,7 +41,7 @@ object Room {
def toReasons(room: Room): Set[Reason] =
room match {
case Cheat => Set(Reason.Cheat)
case Print => Set(Reason.CheatPrint)
case Print => Set(Reason.AltPrint)
case Comm => Set(Reason.Comm)
case Other => Set(Reason.Boost, Reason.Other)
case Xfiles => Set.empty
@ -52,14 +52,14 @@ object Room {
def highest = ~value.values.maxOption
}
def isGrantedFor(mod: User)(room: Room) = {
def isGrantedFor(mod: Holder)(room: Room) = {
import lila.security.Granter
room match {
case Cheat => Granter(_.MarkEngine)(mod)
case Print => Granter(_.ViewIpPrint)(mod)
case Comm => Granter(_.Shadowban)(mod)
case Other => Granter(_.MarkBooster)(mod)
case Xfiles => Granter(_.MarkEngine)(mod)
case Cheat => Granter.is(_.MarkEngine)(mod)
case Print => Granter.is(_.Admin)(mod)
case Comm => Granter.is(_.Shadowban)(mod)
case Other => Granter.is(_.MarkBooster)(mod)
case Xfiles => Granter.is(_.MarkEngine)(mod)
}
}
}

View File

@ -20,7 +20,7 @@ final private[round] class Drawer(
Pov(game)
.map { pov =>
import Pref.PrefZero
if (game.playerHasOfferedDraw(pov.color)) fuccess(pov.some)
if (game.playerHasOfferedDrawRecently(pov.color)) fuccess(pov.some)
else
pov.player.userId ?? prefApi.getPref map { pref =>
pref.autoThreefold == Pref.AutoThreefold.ALWAYS || {
@ -41,9 +41,7 @@ final private[round] class Drawer(
case Pov(g, color) if g playerCanOfferDraw color =>
proxy.save {
messenger.system(g, color.fold(trans.whiteOffersDraw, trans.blackOffersDraw).txt())
Progress(g) map { g =>
g.updatePlayer(color, _ offerDraw g.turns)
}
Progress(g) map { _ offerDraw color }
} >>- publishDrawOffer(pov) inject List(Event.DrawOffer(by = color.some))
case _ => fuccess(List(Event.ReloadOwner))
}

View File

@ -203,13 +203,10 @@ final private[round] class RoundDuct(
"analysis" -> lila.analyse.JsonView.bothPlayers(a.game, a.analysis),
"tree" -> lila.tree.Node.minimalNodeJsonWriter.writes {
TreeBuilder(
id = a.analysis.id,
pgnMoves = a.game.pgnMoves,
variant = a.variant,
analysis = a.analysis.some,
initialFen = a.initialFen,
withFlags = JsonView.WithFlags(),
clocks = none
a.game,
a.analysis.some,
a.initialFen,
JsonView.WithFlags()
)
}
)

View File

@ -1,6 +1,6 @@
package lila.round
import chess.Centis
import chess.{ Centis, Color }
import chess.format.pgn.Glyphs
import chess.format.{ FEN, Forsyth, Uci, UciCharPair }
import chess.opening._
@ -26,32 +26,14 @@ object TreeBuilder {
analysis: Option[Analysis],
initialFen: FEN,
withFlags: WithFlags
): Root =
apply(
id = game.id,
pgnMoves = game.pgnMoves,
variant = game.variant,
analysis = analysis,
initialFen = initialFen,
withFlags = withFlags,
clocks = withFlags.clocks ?? game.bothClockStates
)
def apply(
id: String,
pgnMoves: Vector[String],
variant: Variant,
analysis: Option[Analysis],
initialFen: FEN,
withFlags: WithFlags,
clocks: Option[Vector[Centis]]
): Root = {
val withClocks: Option[Vector[Centis]] = withFlags.clocks ?? clocks
chess.Replay.gameMoveWhileValid(pgnMoves, initialFen, variant) match {
val withClocks: Option[Vector[Centis]] = withFlags.clocks ?? game.bothClockStates
val drawOfferPlies = game.drawOffers.normalizedPlies
chess.Replay.gameMoveWhileValid(game.pgnMoves, initialFen, game.variant) match {
case (init, games, error) =>
error foreach logChessError(id)
error foreach logChessError(game.id)
val openingOf: OpeningOf =
if (withFlags.opening && Variant.openingSensibleVariants(variant)) FullOpeningDB.findByFen
if (withFlags.opening && Variant.openingSensibleVariants(game.variant)) FullOpeningDB.findByFen
else _ => None
val fen = Forsyth >> init
val infos: Vector[Info] = analysis.??(_.infos.toVector)
@ -83,18 +65,18 @@ object TreeBuilder {
eval = info map makeEval,
glyphs = Glyphs.fromList(advice.map(_.judgment.glyph).toList),
comments = Node.Comments {
advice.map(_.makeComment(withEval = false, withBestMove = true)).toList.map { text =>
Node.Comment(
Node.Comment.Id.make,
Node.Comment.Text(text),
Node.Comment.Author.Lichess
)
}
drawOfferPlies(g.turns)
.option(makeLichessComment(s"${!Color.fromPly(g.turns)} offers draw"))
.toList :::
advice
.map(_.makeComment(withEval = false, withBestMove = true))
.toList
.map(makeLichessComment)
}
)
advices.get(g.turns + 1).flatMap { adv =>
games.lift(index - 1).map { case (fromGame, _) =>
withAnalysisChild(id, branch, variant, Forsyth >> fromGame, openingOf)(adv.info)
withAnalysisChild(game.id, branch, game.variant, Forsyth >> fromGame, openingOf)(adv.info)
}
} getOrElse branch
}
@ -108,6 +90,13 @@ object TreeBuilder {
}
}
private def makeLichessComment(text: String) =
Node.Comment(
Node.Comment.Id.make,
Node.Comment.Text(text),
Node.Comment.Author.Lichess
)
private def withAnalysisChild(
id: String,
root: Branch,

View File

@ -1,6 +1,6 @@
package lila.security
import lila.user.User
import lila.user.{ Holder, User }
object Granter {
@ -10,21 +10,33 @@ object Granter {
def apply(f: Permission.Selector)(user: User): Boolean =
apply(f(Permission), user.roles)
def is(permission: Permission)(holder: Holder): Boolean =
apply(permission)(holder.user)
def is(f: Permission.Selector)(holder: Holder): Boolean =
apply(f)(holder.user)
def apply(permission: Permission, roles: Seq[String]): Boolean =
Permission(roles).exists(_ is permission)
def byRoles(f: Permission.Selector)(roles: Seq[String]): Boolean =
apply(f(Permission), roles)
def canGrant(user: User, permission: Permission): Boolean =
apply(_.SuperAdmin)(user) || {
apply(_.ChangePermission)(user) && Permission.nonModPermissions(permission)
def canGrant(user: Holder, permission: Permission): Boolean =
is(_.SuperAdmin)(user) || {
is(_.ChangePermission)(user) && Permission.nonModPermissions(permission)
} || {
apply(_.Admin)(user) && {
apply(permission)(user) || Set[Permission](
is(_.Admin)(user) && {
is(permission)(user) || Set[Permission](
Permission.MonitoredMod,
Permission.PublicMod
)(permission)
}
}
def canViewAltUsername(mod: Holder, user: User): Boolean =
is(_.Admin)(mod) || {
(is(_.Hunter)(mod) && user.marks.engine) ||
(is(_.Shusher)(mod) && user.marks.troll)
}
}

View File

@ -25,8 +25,9 @@ object Permission {
case object SetKidMode extends Permission("SET_KID_MODE", List(UserModView), "Set Kid Mode")
case object MarkEngine extends Permission("ADJUST_CHEATER", List(UserModView), "Mark as cheater")
case object MarkBooster extends Permission("ADJUST_BOOSTER", List(UserModView), "Mark as booster")
case object IpBan extends Permission("IP_BAN", List(UserModView), "IP ban")
case object IpBan extends Permission("IP_BAN", List(UserModView, ViewPrintNoIP), "IP ban")
case object PrintBan extends Permission("PRINT_BAN", List(UserModView), "Print ban")
case object ViewPrintNoIP extends Permission("VIEW_PRINT_NOIP", "View Print & NoIP")
case object DisableTwoFactor extends Permission("DISABLE_2FA", "Disable 2FA")
case object CloseAccount extends Permission("CLOSE_ACCOUNT", List(UserModView), "Close/reopen account")
case object SetTitle extends Permission("SET_TITLE", List(UserModView), "Set/unset title")
@ -50,7 +51,6 @@ object Permission {
case object Coach extends Permission("COACH", "Is a coach")
case object Teacher extends Permission("TEACHER", "Is a class teacher")
case object ModNote extends Permission("MOD_NOTE", "Mod notes")
case object ViewIpPrint extends Permission("VIEW_IP_PRINT", "View IP/print")
case object RemoveRanking extends Permission("REMOVE_RANKING", "Remove from ranking")
case object ReportBan extends Permission("REPORT_BAN", "Report ban")
case object ModMessage extends Permission("MOD_MESSAGE", "Send mod messages")
@ -92,7 +92,8 @@ object Permission {
UserSearch,
RemoveRanking,
ModMessage,
ModNote
ModNote,
ViewPrintNoIP
),
"Hunter"
)
@ -110,20 +111,12 @@ object Permission {
ModMessage,
SeeReport,
ModLog,
ModNote
ModNote,
ViewPrintNoIP
),
"Shusher"
)
case object Doxing
extends Permission(
"DOXING",
List(
ViewIpPrint
),
"Doxing"
)
case object Admin
extends Permission(
"ADMIN",
@ -131,7 +124,6 @@ object Permission {
Hunter,
Shusher,
Appeals,
Doxing,
IpBan,
PrintBan,
CloseAccount,
@ -190,7 +182,6 @@ object Permission {
),
"Account mod" -> List(
UserModView,
ViewIpPrint,
IpBan,
PrintBan,
DisableTwoFactor,
@ -242,7 +233,6 @@ object Permission {
LichessTeam,
Hunter,
Shusher,
Doxing,
Admin,
SuperAdmin
)

View File

@ -11,7 +11,7 @@ import lila.common.Bus
import lila.hub.actorApi.timeline.{ Propagate, StudyCreate, StudyLike }
import lila.socket.Socket.Sri
import lila.tree.Node.{ Comment, Gamebook, Shapes }
import lila.user.User
import lila.user.{ Holder, User }
final class StudyApi(
studyRepo: StudyRepo,
@ -832,7 +832,7 @@ final class StudyApi(
}
}
def adminInvite(studyId: Study.Id, me: User): Funit =
def adminInvite(studyId: Study.Id, me: Holder): Funit =
sequenceStudy(studyId) { inviter.admin(_, me) }
def erase(user: User) =

View File

@ -6,7 +6,7 @@ import lila.db.dsl._
import lila.notify.{ InvitedToStudy, Notification, NotifyApi }
import lila.pref.Pref
import lila.relation.{ Block, Follow }
import lila.user.User
import lila.user.{ Holder, User }
final private class StudyInvite(
studyRepo: StudyRepo,
@ -72,7 +72,7 @@ final private class StudyInvite(
}(funit)
} yield invited
def admin(study: Study, user: User): Funit =
def admin(study: Study, user: Holder): Funit =
studyRepo.coll {
_.update
.one(

View File

@ -12,6 +12,7 @@ case class Team(
location: Option[String],
password: Option[String],
description: String,
descPrivate: Option[String],
nbMembers: Int,
enabled: Boolean,
open: Boolean,
@ -82,6 +83,7 @@ object Team {
location: Option[String],
password: Option[String],
description: String,
descPrivate: Option[String],
open: Boolean,
createdBy: User
): Team =
@ -91,6 +93,7 @@ object Team {
location = location,
password = password,
description = description,
descPrivate = descPrivate,
nbMembers = 1,
enabled = true,
open = open,

View File

@ -53,6 +53,7 @@ final class TeamApi(
location = s.location,
password = s.password,
description = s.description,
descPrivate = s.descPrivate,
open = s.isOpen,
createdBy = me
)
@ -74,6 +75,7 @@ final class TeamApi(
location = e.location,
password = e.password,
description = e.description,
descPrivate = e.descPrivate,
open = e.isOpen,
chat = e.chat
) pipe { team =>

View File

@ -4,7 +4,7 @@ import play.api.data._
import play.api.data.Forms._
import scala.concurrent.duration._
import lila.common.Form.{ cleanText, numberIn }
import lila.common.Form.{ cleanNonEmptyText, cleanText, numberIn }
import lila.db.dsl._
final private[team] class TeamForm(
@ -26,6 +26,7 @@ final private[team] class TeamForm(
"message" -> optional(cleanText(minLength = 30, maxLength = 2000))
.verifying("Request message required", msg => msg.isDefined || team.open)
val description = "description" -> cleanText(minLength = 30, maxLength = 4000)
val descPrivate = "descPrivate" -> optional(cleanNonEmptyText(maxLength = 4000))
val request = "request" -> boolean
val gameId = "gameId" -> text
val move = "move" -> text
@ -38,6 +39,7 @@ final private[team] class TeamForm(
Fields.location,
Fields.password,
Fields.description,
Fields.descPrivate,
Fields.request,
Fields.gameId,
Fields.move
@ -52,6 +54,7 @@ final private[team] class TeamForm(
Fields.location,
Fields.password,
Fields.description,
Fields.descPrivate,
Fields.request,
Fields.chat
)(TeamEdit.apply)(TeamEdit.unapply)
@ -59,6 +62,7 @@ final private[team] class TeamForm(
location = team.location,
password = team.password,
description = team.description,
descPrivate = team.descPrivate,
request = !team.open,
chat = team.chat
)
@ -114,6 +118,7 @@ private[team] case class TeamSetup(
location: Option[String],
password: Option[String],
description: String,
descPrivate: Option[String],
request: Boolean,
gameId: String,
move: String
@ -125,7 +130,8 @@ private[team] case class TeamSetup(
copy(
name = name.trim,
location = location map (_.trim) filter (_.nonEmpty),
description = description.trim
description = description.trim,
descPrivate = descPrivate map (_.trim) filter (_.nonEmpty)
)
}
@ -133,6 +139,7 @@ private[team] case class TeamEdit(
location: Option[String],
password: Option[String],
description: String,
descPrivate: Option[String],
request: Boolean,
chat: Team.ChatFor
) {
@ -142,7 +149,8 @@ private[team] case class TeamEdit(
def trim =
copy(
location = location map (_.trim) filter (_.nonEmpty),
description = description.trim
description = description.trim,
descPrivate = descPrivate map (_.trim) filter (_.nonEmpty)
)
}

View File

@ -52,7 +52,6 @@ final private[timeline] class Push(
private def modPermissions =
List(
Permission.ModNote,
Permission.Doxing,
Permission.Admin,
Permission.SuperAdmin
)

View File

@ -17,6 +17,7 @@ object UserMark {
val indexed: Map[String, UserMark] = all.view.map { m =>
m.key -> m
}.toMap
val bannable: Set[UserMark] = Set(Boost, Engine, Troll, Alt)
implicit val markBsonHandler = stringAnyValHandler[UserMark](_.key, indexed.apply)
}
@ -30,7 +31,7 @@ case class UserMarks(value: List[UserMark]) extends AnyVal {
def alt = apply(UserMark.Alt)
def nonEmpty = value.nonEmpty option this
def clean = value.isEmpty
def clean = !value.exists(UserMark.bannable.contains)
def set(sel: UserMark.type => UserMark, v: Boolean) =
UserMarks {

View File

@ -255,9 +255,14 @@ final class UserRepo(val coll: Coll)(implicit ec: scala.concurrent.ExecutionCont
val disabledSelect = $doc(F.enabled -> false)
def markSelect(mark: UserMark)(v: Boolean): Bdoc =
if (v) $doc(F.marks -> mark.key)
else F.marks $ne (mark.key)
else F.marks $ne mark.key
def engineSelect = markSelect(UserMark.Engine) _
def trollSelect = markSelect(UserMark.Troll) _
val lameOrTroll = $or(
$doc(F.marks -> UserMark.Engine.key),
$doc(F.marks -> UserMark.Boost.key),
$doc(F.marks -> UserMark.Troll.key)
)
def stablePerfSelect(perf: String) =
$doc(s"perfs.$perf.gl.d" -> $lt(lila.rating.Glicko.provisionalDeviation))
val patronSelect = $doc(s"${F.plan}.active" -> true)
@ -597,6 +602,9 @@ final class UserRepo(val coll: Coll)(implicit ec: scala.concurrent.ExecutionCont
def countEngines(userIds: List[User.ID]): Fu[Int] =
coll.secondaryPreferred.countSel($inIds(userIds) ++ engineSelect(true))
def countLameOrTroll(userIds: List[User.ID]): Fu[Int] =
coll.secondaryPreferred.countSel($inIds(userIds) ++ lameOrTroll)
def containsEngine(userIds: List[User.ID]): Fu[Boolean] =
coll.exists($inIds(userIds) ++ engineSelect(true))

View File

@ -3,3 +3,8 @@ package lila.user
final class GetBotIds(f: () => Fu[Set[User.ID]]) extends (() => Fu[Set[User.ID]]) {
def apply() = f()
}
// permission holder
case class Holder(user: User) extends AnyVal {
def id = user.id
}

View File

@ -15,7 +15,7 @@ object Dependencies {
val scrimage = "com.sksamuel.scrimage" % "scrimage-core" % "4.0.17"
val scaffeine = "com.github.blemale" %% "scaffeine" % "4.0.2" % "compile"
val googleOAuth = "com.google.auth" % "google-auth-library-oauth2-http" % "0.24.1"
val scalaUri = "io.lemonlabs" %% "scala-uri" % "2.3.1"
val scalaUri = "io.lemonlabs" %% "scala-uri" % "3.1.0"
val scalatags = "com.lihaoyi" %% "scalatags" % "0.9.3"
val lettuce = "io.lettuce" % "lettuce-core" % "5.3.6.RELEASE"
val epoll = "io.netty" % "netty-transport-native-epoll" % "4.1.58.Final" classifier "linux-x86_64"

View File

@ -1 +1 @@
sbt.version=1.4.7
sbt.version=1.4.9

View File

@ -1,45 +0,0 @@
$(function () {
var autoRefreshEnabled = true;
var autoRefreshOnHold = false;
var renderButton = function () {
$('#auto_refresh').toggleClass('active', autoRefreshEnabled).toggleClass('hold', autoRefreshOnHold);
};
var onPageReload = function () {
$('#communication').append(
$('<a id="auto_refresh" class="button">Auto refresh</a>').on('click', () => {
autoRefreshEnabled = !autoRefreshEnabled;
renderButton();
})
);
renderButton();
$('#communication .chat').each(function () {
this.scrollTop = 99999;
});
$('#communication')
.on('mouseenter', '.chat', function () {
autoRefreshOnHold = true;
$('#auto_refresh').addClass('hold');
})
.on('mouseleave', '.chat', function () {
autoRefreshOnHold = false;
$('#auto_refresh').removeClass('hold');
});
};
onPageReload();
setInterval(function () {
if (!autoRefreshEnabled || document.visibilityState === 'hidden' || autoRefreshOnHold) return;
// Reload only the chat grid portions of the page
fetch('/mod/public-chat')
.then(r => r.text())
.then(html => {
$(html).find('#communication').appendTo($('#comm-wrap').empty());
onPageReload();
});
}, 5000);
});

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More