implement resource link checker

pull/7194/head
Thibault Duplessis 2020-08-26 13:00:53 +02:00
parent ae9d53d472
commit 4aec266719
15 changed files with 209 additions and 37 deletions

View File

@ -7,7 +7,9 @@ import play.api.{ Configuration, Mode }
import scala.concurrent.duration._ import scala.concurrent.duration._
import lila.common.config._ import lila.common.config._
import lila.common.Bus
import lila.user.User import lila.user.User
import lila.chat.GetLinkCheck
@Module @Module
final class Env( final class Env(
@ -55,6 +57,7 @@ final class Env(
val config = ApiConfig loadFrom appConfig val config = ApiConfig loadFrom appConfig
import config.apiToken import config.apiToken
import net.domain
lazy val pgnDump: PgnDump = wire[PgnDump] lazy val pgnDump: PgnDump = wire[PgnDump]
@ -85,6 +88,12 @@ final class Env(
) )
if (mode == Mode.Prod) system.scheduler.scheduleOnce(5 seconds)(influxEvent.start()) if (mode == Mode.Prod) system.scheduler.scheduleOnce(5 seconds)(influxEvent.start())
private lazy val linkCheck = wire[LinkCheck]
Bus.subscribeFun("chatLinkCheck") {
case GetLinkCheck(line, source, promise) => promise completeWith linkCheck(line, source)
}
system.scheduler.scheduleWithFixedDelay(1 minute, 1 minute) { () => system.scheduler.scheduleWithFixedDelay(1 minute, 1 minute) { () =>
lila.mon.bus.classifiers.update(lila.common.Bus.size) lila.mon.bus.classifiers.update(lila.common.Bus.size)
// ensure the Lichess user is online // ensure the Lichess user is online

View File

@ -0,0 +1,130 @@
package lila.api
import cats.implicits._
import scala.concurrent.ExecutionContext
import lila.chat.UserLine
import lila.common.config.NetDomain
import lila.hub.actorApi.shutup.PublicSource
import lila.simul.Simul
import lila.simul.SimulApi
import lila.swiss.Swiss
import lila.swiss.SwissApi
import lila.team.Team
import lila.team.TeamRepo
import lila.tournament.Tournament
import lila.tournament.TournamentRepo
import lila.user.User
import lila.study.Study
import lila.study.StudyRepo
/* Determine if a link to a lichess resource
* can be posted from another lichess resource.
* Owners of a resource can post any link on it (but not to it).
* Links to a team resource can be posted from another resource of the same team.
* Links to official resources can be posted from anywhere.
* */
final private class LinkCheck(
domain: NetDomain,
teamRepo: TeamRepo,
tournamentRepo: TournamentRepo,
simulApi: SimulApi,
swissApi: SwissApi,
studyRepo: StudyRepo
)(implicit ec: ExecutionContext) {
import LinkCheck._
def apply(line: UserLine, source: PublicSource): Fu[Boolean] = {
if (multipleLinks find line.text) fuFalse
else
line.text match {
case tournamentLinkR(id) => withSource(source, tourLink)(id, line)
case simulLinkR(id) => withSource(source, simulLink)(id, line)
case swissLinkR(id) => withSource(source, swissLink)(id, line)
case studyLinkR(id) => withSource(source, studyLink)(id, line)
case _ => fuTrue
}
}
private def withSource(
source: PublicSource,
f: (String, FullSource) => Fu[Boolean]
)(id: String, line: UserLine): Fu[Boolean] = {
source match {
case PublicSource.Tournament(id) => tournamentRepo byId id map2 FullSource.TournamentSource
case PublicSource.Simul(id) => simulApi find id map2 FullSource.SimulSource
case PublicSource.Swiss(id) => swissApi byId Swiss.Id(id) map2 FullSource.SwissSource
case PublicSource.Team(id) => teamRepo byId id map2 FullSource.TeamSource
case PublicSource.Study(id) => studyRepo byId Study.Id(id) map2 FullSource.StudySource
case _ => fuccess(none)
}
} flatMap {
_ ?? { source =>
// the owners of a chat can post whichever link they like
if (source.owners(line.userId)) fuTrue
else f(id, source)
}
}
private def tourLink(tourId: Tournament.ID, source: FullSource) =
tournamentRepo byId tourId flatMap {
_ ?? { tour =>
fuccess(tour.isScheduled) >>| {
source.teamId ?? { sourceTeamId =>
fuccess(tour.conditions.teamMember.exists(_.teamId == sourceTeamId)) >>|
tournamentRepo.isForTeam(tour.id, sourceTeamId)
}
}
}
}
private def simulLink(simulId: Tournament.ID, source: FullSource) =
simulApi teamOf simulId map {
_ exists source.teamId.has
}
private def swissLink(swissId: String, source: FullSource) =
swissApi teamOf Swiss.Id(swissId) map {
_ exists source.teamId.has
}
private def studyLink(studyId: String, source: FullSource) = fuFalse
private val multipleLinks = s"$domain.+$domain".r.unanchored
private val tournamentLinkR = s"$domain/tournament/(\\w+)".r.unanchored
private val simulLinkR = s"$domain/simul/(\\w+)".r.unanchored
private val swissLinkR = s"$domain/swiss/(\\w+)".r.unanchored
private val studyLinkR = s"$domain/study/(\\w+)".r.unanchored
}
private object LinkCheck {
sealed trait FullSource {
def owners: Set[User.ID]
def teamId: Option[Team.ID]
}
object FullSource {
case class TournamentSource(value: Tournament) extends FullSource {
def owners = Set(value.createdBy)
def teamId = value.conditions.teamMember.map(_.teamId)
}
case class SimulSource(value: Simul) extends FullSource {
def owners = Set(value.hostId)
def teamId = value.team
}
case class SwissSource(value: Swiss) extends FullSource {
def owners = Set(value.createdBy)
def teamId = value.teamId.some
}
case class TeamSource(value: Team) extends FullSource {
def owners = value.leaders
def teamId = value.id.some
}
case class StudySource(value: Study) extends FullSource {
def owners = value.members.idSet
def teamId = none
}
}
}

View File

@ -4,9 +4,9 @@ import chess.Color
import reactivemongo.api.ReadPreference import reactivemongo.api.ReadPreference
import scala.concurrent.duration._ import scala.concurrent.duration._
import lila.common.Bus
import lila.common.config.NetDomain import lila.common.config.NetDomain
import lila.common.String.noShouting import lila.common.String.noShouting
import lila.common.Bus
import lila.db.dsl._ import lila.db.dsl._
import lila.hub.actorApi.shutup.{ PublicSource, RecordPrivateChat, RecordPublicChat } import lila.hub.actorApi.shutup.{ PublicSource, RecordPrivateChat, RecordPublicChat }
import lila.memo.CacheApi._ import lila.memo.CacheApi._
@ -23,7 +23,7 @@ final class ChatApi(
cacheApi: lila.memo.CacheApi, cacheApi: lila.memo.CacheApi,
maxLinesPerChat: Chat.MaxLines, maxLinesPerChat: Chat.MaxLines,
netDomain: NetDomain netDomain: NetDomain
)(implicit ec: scala.concurrent.ExecutionContext) { )(implicit ec: scala.concurrent.ExecutionContext, actorSystem: akka.actor.ActorSystem) {
import Chat.{ chatIdBSONHandler, userChatBSONHandler } import Chat.{ chatIdBSONHandler, userChatBSONHandler }
@ -88,20 +88,31 @@ final class ChatApi(
): Funit = ): Funit =
makeLine(chatId, userId, text) flatMap { makeLine(chatId, userId, text) flatMap {
_ ?? { line => _ ?? { line =>
pushLine(chatId, line) >>- { linkCheck(line, publicSource) flatMap {
if (publicSource.isDefined) cached invalidate chatId case false =>
shutup ! { logger.info(s"Link check rejected $line in $publicSource")
publicSource match { funit
case Some(source) => RecordPublicChat(userId, text, source) case true =>
case _ => RecordPrivateChat(chatId.value, userId, text) pushLine(chatId, line) >>- {
if (publicSource.isDefined) cached invalidate chatId
shutup ! {
publicSource match {
case Some(source) => RecordPublicChat(userId, text, source)
case _ => RecordPrivateChat(chatId.value, userId, text)
}
}
publish(chatId, actorApi.ChatLine(chatId, line), busChan)
lila.mon.chat.message(publicSource.fold("player")(_.parentName), line.troll).increment()
} }
}
publish(chatId, actorApi.ChatLine(chatId, line), busChan)
lila.mon.chat.message(publicSource.fold("player")(_.parentName), line.troll).increment()
} }
} }
} }
private def linkCheck(line: UserLine, source: Option[PublicSource]) =
source.fold(fuccess(true)) { s =>
Bus.ask[Boolean]("chatLinkCheck") { GetLinkCheck(line, s, _) }
}
def clear(chatId: Chat.Id) = coll.delete.one($id(chatId)).void def clear(chatId: Chat.Id) = coll.delete.one($id(chatId)).void
def system(chatId: Chat.Id, text: String, busChan: BusChan.Select): Funit = { def system(chatId: Chat.Id, text: String, busChan: BusChan.Select): Funit = {

View File

@ -1,6 +1,8 @@
package lila.chat package lila.chat
import lila.user.User import lila.user.User
import scala.concurrent.Promise
import lila.hub.actorApi.shutup.PublicSource
case class UserModInfo( case class UserModInfo(
user: User, user: User,
@ -21,3 +23,5 @@ object BusChan {
type Select = BusChan.type => BusChan type Select = BusChan.type => BusChan
} }
case class GetLinkCheck(line: UserLine, source: PublicSource, promise: Promise[Boolean])

View File

@ -67,7 +67,7 @@ object HTTPRequest {
case class UaMatcher(rStr: String) { case class UaMatcher(rStr: String) {
private val regex = rStr.r private val regex = rStr.r
def apply(req: RequestHeader): Boolean = userAgent(req) ?? { regex.find(_) } def apply(req: RequestHeader): Boolean = userAgent(req) ?? regex.find
} }
def isFishnet(req: RequestHeader) = req.path startsWith "/fishnet/" def isFishnet(req: RequestHeader) = req.path startsWith "/fishnet/"

View File

@ -9,7 +9,7 @@ import lila.common.config.NetDomain
final class PromotionApi(domain: NetDomain) { final class PromotionApi(domain: NetDomain) {
def test(user: User)(text: String): Boolean = def test(user: User)(text: String): Boolean =
user.isVerified || { user.isVerified || user.isAdmin || {
val promotions = extract(text) val promotions = extract(text)
promotions.isEmpty || { promotions.isEmpty || {
val prev = ~cache.getIfPresent(user.id) val prev = ~cache.getIfPresent(user.id)
@ -35,6 +35,8 @@ final class PromotionApi(domain: NetDomain) {
s"$domain/team/([\\w-]+)", s"$domain/team/([\\w-]+)",
s"$domain/tournament/(\\w+)", s"$domain/tournament/(\\w+)",
s"$domain/swiss/(\\w+)", s"$domain/swiss/(\\w+)",
s"$domain/simul/(\\w+)",
s"$domain/study/(\\w+)",
"""(?:youtube\.com|youtu\.be)/(?:watch)?(?:\?v=)?([^"&?/ ]{11})""", """(?:youtube\.com|youtu\.be)/(?:watch)?(?:\?v=)?([^"&?/ ]{11})""",
"""youtube\.com/channel/([\w-]{24})""", """youtube\.com/channel/([\w-]{24})""",
"""twitch\.tv/([a-zA-Z0-9](?:\w{2,24}+))""" """twitch\.tv/([a-zA-Z0-9](?:\w{2,24}+))"""

View File

@ -17,7 +17,7 @@ case class Simul(
variants: List[Variant], variants: List[Variant],
position: Option[StartingPosition], position: Option[StartingPosition],
createdAt: DateTime, createdAt: DateTime,
hostId: String, hostId: User.ID,
hostRating: Int, hostRating: Int,
hostGameId: Option[String], // game the host is focusing on hostGameId: Option[String], // game the host is focusing on
startedAt: Option[DateTime], startedAt: Option[DateTime],

View File

@ -6,8 +6,10 @@ import play.api.libs.json.Json
import scala.concurrent.duration._ import scala.concurrent.duration._
import lila.common.{ Bus, Debouncer } import lila.common.{ Bus, Debouncer }
import lila.db.dsl._
import lila.game.{ Game, GameRepo, PerfPicker } import lila.game.{ Game, GameRepo, PerfPicker }
import lila.hub.actorApi.timeline.{ Propagate, SimulCreate, SimulJoin } import lila.hub.actorApi.timeline.{ Propagate, SimulCreate, SimulJoin }
import lila.hub.LightTeam.TeamID
import lila.memo.CacheApi._ import lila.memo.CacheApi._
import lila.socket.Socket.SendToFlag import lila.socket.Socket.SendToFlag
import lila.user.{ User, UserRepo } import lila.user.{ User, UserRepo }
@ -222,6 +224,9 @@ final class SimulApi(
def idToName(id: Simul.ID): Fu[Option[String]] = def idToName(id: Simul.ID): Fu[Option[String]] =
repo find id dmap2 { _.fullName } repo find id dmap2 { _.fullName }
def teamOf(id: Simul.ID): Fu[Option[TeamID]] =
repo.coll.primitiveOne[TeamID]($id(id), "team")
private def makeGame(simul: Simul, host: User)( private def makeGame(simul: Simul, host: User)(
pairingAndNumber: (SimulPairing, Int) pairingAndNumber: (SimulPairing, Int)
): Fu[(Game, chess.Color)] = ): Fu[(Game, chess.Color)] =

View File

@ -10,7 +10,7 @@ import lila.db.BSON.BSONJodaDateTimeHandler
import lila.db.dsl._ import lila.db.dsl._
import lila.user.User import lila.user.User
final private[simul] class SimulRepo(simulColl: Coll)(implicit ec: scala.concurrent.ExecutionContext) { final private[simul] class SimulRepo(val coll: Coll)(implicit ec: scala.concurrent.ExecutionContext) {
implicit private val SimulStatusBSONHandler = tryHandler[SimulStatus]( implicit private val SimulStatusBSONHandler = tryHandler[SimulStatus](
{ case BSONInteger(v) => SimulStatus(v) toTry s"No such simul status: $v" }, { case BSONInteger(v) => SimulStatus(v) toTry s"No such simul status: $v" },
@ -57,13 +57,13 @@ final private[simul] class SimulRepo(simulColl: Coll)(implicit ec: scala.concurr
private val createdSort = $sort desc "createdAt" private val createdSort = $sort desc "createdAt"
def find(id: Simul.ID): Fu[Option[Simul]] = def find(id: Simul.ID): Fu[Option[Simul]] =
simulColl.byId[Simul](id) coll.byId[Simul](id)
def byIds(ids: List[Simul.ID]): Fu[List[Simul]] = def byIds(ids: List[Simul.ID]): Fu[List[Simul]] =
simulColl.byIds[Simul](ids) coll.byIds[Simul](ids)
def exists(id: Simul.ID): Fu[Boolean] = def exists(id: Simul.ID): Fu[Boolean] =
simulColl.exists($id(id)) coll.exists($id(id))
def findStarted(id: Simul.ID): Fu[Option[Simul]] = def findStarted(id: Simul.ID): Fu[Option[Simul]] =
find(id) map (_ filter (_.isStarted)) find(id) map (_ filter (_.isStarted))
@ -72,22 +72,22 @@ final private[simul] class SimulRepo(simulColl: Coll)(implicit ec: scala.concurr
find(id) map (_ filter (_.isCreated)) find(id) map (_ filter (_.isCreated))
def findPending(hostId: User.ID): Fu[List[Simul]] = def findPending(hostId: User.ID): Fu[List[Simul]] =
simulColl.list[Simul](createdSelect ++ $doc("hostId" -> hostId)) coll.list[Simul](createdSelect ++ $doc("hostId" -> hostId))
def byTeamLeaders(teamId: String, hostIds: Seq[User.ID]): Fu[List[Simul]] = def byTeamLeaders(teamId: String, hostIds: Seq[User.ID]): Fu[List[Simul]] =
simulColl coll
.find( .find(
createdSelect ++ createdSelect ++
$doc("hostId" $in hostIds, "team" $in List(BSONString(teamId))) $doc("hostId" $in hostIds, "team" $in List(BSONString(teamId)))
) )
.hint(simulColl hint $doc("hostId" -> 1)) .hint(coll hint $doc("hostId" -> 1))
.cursor[Simul]() .cursor[Simul]()
.list() .list()
private val featurableSelect = $doc("featurable" -> true) private val featurableSelect = $doc("featurable" -> true)
def allCreatedFeaturable: Fu[List[Simul]] = def allCreatedFeaturable: Fu[List[Simul]] =
simulColl coll
.find( .find(
// hits partial index hostSeenAt_-1 // hits partial index hostSeenAt_-1
createdSelect ++ featurableSelect ++ $doc( createdSelect ++ featurableSelect ++ $doc(
@ -96,7 +96,7 @@ final private[simul] class SimulRepo(simulColl: Coll)(implicit ec: scala.concurr
) )
) )
.sort(createdSort) .sort(createdSort)
.hint(simulColl hint $doc("hostSeenAt" -> -1)) .hint(coll hint $doc("hostSeenAt" -> -1))
.cursor[Simul]() .cursor[Simul]()
.list() map { .list() map {
_.foldLeft(List.empty[Simul]) { _.foldLeft(List.empty[Simul]) {
@ -106,29 +106,29 @@ final private[simul] class SimulRepo(simulColl: Coll)(implicit ec: scala.concurr
} }
def allStarted: Fu[List[Simul]] = def allStarted: Fu[List[Simul]] =
simulColl coll
.find(startedSelect) .find(startedSelect)
.sort(createdSort) .sort(createdSort)
.cursor[Simul]() .cursor[Simul]()
.list() .list()
def allFinishedFeaturable(max: Int): Fu[List[Simul]] = def allFinishedFeaturable(max: Int): Fu[List[Simul]] =
simulColl coll
.find(finishedSelect ++ featurableSelect) .find(finishedSelect ++ featurableSelect)
.sort($sort desc "finishedAt") .sort($sort desc "finishedAt")
.cursor[Simul]() .cursor[Simul]()
.list(max) .list(max)
def allNotFinished = def allNotFinished =
simulColl.list[Simul]($doc("status" $ne SimulStatus.Finished.id)) coll.list[Simul]($doc("status" $ne SimulStatus.Finished.id))
def create(simul: Simul, featurable: Boolean): Funit = def create(simul: Simul, featurable: Boolean): Funit =
simulColl.insert one { coll.insert one {
SimulBSONHandler.writeTry(simul).get ++ featurable.??(featurableSelect) SimulBSONHandler.writeTry(simul).get ++ featurable.??(featurableSelect)
} void } void
def update(simul: Simul, featurable: Option[Boolean]) = def update(simul: Simul, featurable: Option[Boolean]) =
simulColl.update coll.update
.one( .one(
$id(simul.id), $id(simul.id),
$set(SimulBSONHandler writeTry simul get) ++ featurable.?? { feat => $set(SimulBSONHandler writeTry simul get) ++ featurable.?? { feat =>
@ -138,10 +138,10 @@ final private[simul] class SimulRepo(simulColl: Coll)(implicit ec: scala.concurr
.void .void
def remove(simul: Simul) = def remove(simul: Simul) =
simulColl.delete.one($id(simul.id)).void coll.delete.one($id(simul.id)).void
def setHostGameId(simul: Simul, gameId: String) = def setHostGameId(simul: Simul, gameId: String) =
simulColl.update coll.update
.one( .one(
$id(simul.id), $id(simul.id),
$set("hostGameId" -> gameId) $set("hostGameId" -> gameId)
@ -149,7 +149,7 @@ final private[simul] class SimulRepo(simulColl: Coll)(implicit ec: scala.concurr
.void .void
def setHostSeenNow(simul: Simul) = def setHostSeenNow(simul: Simul) =
simulColl.update coll.update
.one( .one(
$id(simul.id), $id(simul.id),
$set("hostSeenAt" -> DateTime.now) $set("hostSeenAt" -> DateTime.now)
@ -157,7 +157,7 @@ final private[simul] class SimulRepo(simulColl: Coll)(implicit ec: scala.concurr
.void .void
def setText(simul: Simul, text: String) = def setText(simul: Simul, text: String) =
simulColl.update coll.update
.one( .one(
$id(simul.id), $id(simul.id),
$set("text" -> text) $set("text" -> text)
@ -165,7 +165,7 @@ final private[simul] class SimulRepo(simulColl: Coll)(implicit ec: scala.concurr
.void .void
def cleanup = def cleanup =
simulColl.delete.one( coll.delete.one(
createdSelect ++ $doc( createdSelect ++ $doc(
"createdAt" -> $doc("$lt" -> (DateTime.now minusMinutes 60)) "createdAt" -> $doc("$lt" -> (DateTime.now minusMinutes 60))
) )

View File

@ -32,7 +32,8 @@ case class StudyMembers(members: StudyMember.MemberMap) {
def get = members.get _ def get = members.get _
def ids = members.keys def ids = members.keys
def idSet = members.keySet
def contributorIds: Set[User.ID] = def contributorIds: Set[User.ID] =
members.view.collect { members.view.collect {

View File

@ -452,6 +452,9 @@ final class SwissApi(
.sort($sort desc "startsAt") .sort($sort desc "startsAt")
.cursor[Swiss]() .cursor[Swiss]()
def teamOf(id: Swiss.Id): Fu[Option[TeamID]] =
colls.swiss.primitiveOne[TeamID]($id(id), "teamId")
private def recomputeAndUpdateAll(id: Swiss.Id): Funit = private def recomputeAndUpdateAll(id: Swiss.Id): Funit =
scoring(id).flatMap { scoring(id).flatMap {
_ ?? { res => _ ?? { res =>

View File

@ -30,7 +30,7 @@ final class TeamApi(
import BSONHandlers._ import BSONHandlers._
def team(id: Team.ID) = teamRepo.coll.byId[Team](id) def team(id: Team.ID) = teamRepo byId id
def leaderTeam(id: Team.ID) = teamRepo.coll.byId[LeaderTeam](id, $doc("name" -> true)) def leaderTeam(id: Team.ID) = teamRepo.coll.byId[LeaderTeam](id, $doc("name" -> true))

View File

@ -15,6 +15,8 @@ final class TeamRepo(val coll: Coll)(implicit ec: scala.concurrent.ExecutionCont
private val lightProjection = $doc("name" -> true).some private val lightProjection = $doc("name" -> true).some
def byId(id: Team.ID) = coll.byId[Team](id)
def byOrderedIds(ids: Seq[Team.ID]) = coll.byOrderedIds[Team, Team.ID](ids)(_.id) def byOrderedIds(ids: Seq[Team.ID]) = coll.byOrderedIds[Team, Team.ID](ids)(_.id)
def byLeader(id: Team.ID, leaderId: User.ID): Fu[Option[Team]] = def byLeader(id: Team.ID, leaderId: User.ID): Fu[Option[Team]] =

View File

@ -144,6 +144,9 @@ final class TournamentRepo(val coll: Coll, playerCollName: CollName)(implicit
private[tournament] def setForTeam(tourId: Tournament.ID, teamId: TeamID) = private[tournament] def setForTeam(tourId: Tournament.ID, teamId: TeamID) =
coll.update.one($id(tourId), $addToSet("forTeams" -> teamId)) coll.update.one($id(tourId), $addToSet("forTeams" -> teamId))
def isForTeam(tourId: Tournament.ID, teamId: TeamID) =
coll.exists($id(tourId) ++ $doc("forTeams" -> teamId))
private[tournament] def withdrawableIds( private[tournament] def withdrawableIds(
userId: User.ID, userId: User.ID,
teamId: Option[TeamID] = None teamId: Option[TeamID] = None

View File

@ -119,8 +119,10 @@ case class User(
def addRole(role: String) = copy(roles = role :: roles) def addRole(role: String) = copy(roles = role :: roles)
def isVerified = roles.exists(_ contains "ROLE_VERIFIED") def isVerified = roles.exists(_ contains "ROLE_VERIFIED")
def isApiHog = roles.exists(_ contains "ROLE_API_HOG") def isSuperAdmin = roles.exists(_ contains "ROLE_SUPER_ADMIN")
def isAdmin = roles.exists(_ contains "ROLE_ADMIN") || isSuperAdmin
def isApiHog = roles.exists(_ contains "ROLE_API_HOG")
} }
object User { object User {