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
commit
d31d9fa864
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 ())
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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] =
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) =
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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 aren’t 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"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -312,6 +312,9 @@ object faq {
|
|||
whyAreRatingHigher.txt(),
|
||||
p(
|
||||
whyAreRatingHigherExplanation()
|
||||
),
|
||||
p(
|
||||
a(href := routes.Page.loneBookmark("rating-systems"))("More about rating systems")
|
||||
)
|
||||
),
|
||||
question(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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) =
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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) && {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)}*_",
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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)}"
|
||||
)
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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]] =
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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) =
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -52,7 +52,6 @@ final private[timeline] class Push(
|
|||
private def modPermissions =
|
||||
List(
|
||||
Permission.ModNote,
|
||||
Permission.Doxing,
|
||||
Permission.Admin,
|
||||
Permission.SuperAdmin
|
||||
)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1 +1 @@
|
|||
sbt.version=1.4.7
|
||||
sbt.version=1.4.9
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue