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 lila.common.config._
import lila.common.Bus
import lila.user.User
import lila.chat.GetLinkCheck
@Module
final class Env(
@ -55,6 +57,7 @@ final class Env(
val config = ApiConfig loadFrom appConfig
import config.apiToken
import net.domain
lazy val pgnDump: PgnDump = wire[PgnDump]
@ -85,6 +88,12 @@ final class Env(
)
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) { () =>
lila.mon.bus.classifiers.update(lila.common.Bus.size)
// 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 scala.concurrent.duration._
import lila.common.Bus
import lila.common.config.NetDomain
import lila.common.String.noShouting
import lila.common.Bus
import lila.db.dsl._
import lila.hub.actorApi.shutup.{ PublicSource, RecordPrivateChat, RecordPublicChat }
import lila.memo.CacheApi._
@ -23,7 +23,7 @@ final class ChatApi(
cacheApi: lila.memo.CacheApi,
maxLinesPerChat: Chat.MaxLines,
netDomain: NetDomain
)(implicit ec: scala.concurrent.ExecutionContext) {
)(implicit ec: scala.concurrent.ExecutionContext, actorSystem: akka.actor.ActorSystem) {
import Chat.{ chatIdBSONHandler, userChatBSONHandler }
@ -88,20 +88,31 @@ final class ChatApi(
): Funit =
makeLine(chatId, userId, text) flatMap {
_ ?? { line =>
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)
linkCheck(line, publicSource) flatMap {
case false =>
logger.info(s"Link check rejected $line in $publicSource")
funit
case true =>
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 system(chatId: Chat.Id, text: String, busChan: BusChan.Select): Funit = {

View File

@ -1,6 +1,8 @@
package lila.chat
import lila.user.User
import scala.concurrent.Promise
import lila.hub.actorApi.shutup.PublicSource
case class UserModInfo(
user: User,
@ -21,3 +23,5 @@ object 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) {
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/"

View File

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

View File

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

View File

@ -6,8 +6,10 @@ import play.api.libs.json.Json
import scala.concurrent.duration._
import lila.common.{ Bus, Debouncer }
import lila.db.dsl._
import lila.game.{ Game, GameRepo, PerfPicker }
import lila.hub.actorApi.timeline.{ Propagate, SimulCreate, SimulJoin }
import lila.hub.LightTeam.TeamID
import lila.memo.CacheApi._
import lila.socket.Socket.SendToFlag
import lila.user.{ User, UserRepo }
@ -222,6 +224,9 @@ final class SimulApi(
def idToName(id: Simul.ID): Fu[Option[String]] =
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)(
pairingAndNumber: (SimulPairing, Int)
): Fu[(Game, chess.Color)] =

View File

@ -10,7 +10,7 @@ import lila.db.BSON.BSONJodaDateTimeHandler
import lila.db.dsl._
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](
{ 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"
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]] =
simulColl.byIds[Simul](ids)
coll.byIds[Simul](ids)
def exists(id: Simul.ID): Fu[Boolean] =
simulColl.exists($id(id))
coll.exists($id(id))
def findStarted(id: Simul.ID): Fu[Option[Simul]] =
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))
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]] =
simulColl
coll
.find(
createdSelect ++
$doc("hostId" $in hostIds, "team" $in List(BSONString(teamId)))
)
.hint(simulColl hint $doc("hostId" -> 1))
.hint(coll hint $doc("hostId" -> 1))
.cursor[Simul]()
.list()
private val featurableSelect = $doc("featurable" -> true)
def allCreatedFeaturable: Fu[List[Simul]] =
simulColl
coll
.find(
// hits partial index hostSeenAt_-1
createdSelect ++ featurableSelect ++ $doc(
@ -96,7 +96,7 @@ final private[simul] class SimulRepo(simulColl: Coll)(implicit ec: scala.concurr
)
)
.sort(createdSort)
.hint(simulColl hint $doc("hostSeenAt" -> -1))
.hint(coll hint $doc("hostSeenAt" -> -1))
.cursor[Simul]()
.list() map {
_.foldLeft(List.empty[Simul]) {
@ -106,29 +106,29 @@ final private[simul] class SimulRepo(simulColl: Coll)(implicit ec: scala.concurr
}
def allStarted: Fu[List[Simul]] =
simulColl
coll
.find(startedSelect)
.sort(createdSort)
.cursor[Simul]()
.list()
def allFinishedFeaturable(max: Int): Fu[List[Simul]] =
simulColl
coll
.find(finishedSelect ++ featurableSelect)
.sort($sort desc "finishedAt")
.cursor[Simul]()
.list(max)
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 =
simulColl.insert one {
coll.insert one {
SimulBSONHandler.writeTry(simul).get ++ featurable.??(featurableSelect)
} void
def update(simul: Simul, featurable: Option[Boolean]) =
simulColl.update
coll.update
.one(
$id(simul.id),
$set(SimulBSONHandler writeTry simul get) ++ featurable.?? { feat =>
@ -138,10 +138,10 @@ final private[simul] class SimulRepo(simulColl: Coll)(implicit ec: scala.concurr
.void
def remove(simul: Simul) =
simulColl.delete.one($id(simul.id)).void
coll.delete.one($id(simul.id)).void
def setHostGameId(simul: Simul, gameId: String) =
simulColl.update
coll.update
.one(
$id(simul.id),
$set("hostGameId" -> gameId)
@ -149,7 +149,7 @@ final private[simul] class SimulRepo(simulColl: Coll)(implicit ec: scala.concurr
.void
def setHostSeenNow(simul: Simul) =
simulColl.update
coll.update
.one(
$id(simul.id),
$set("hostSeenAt" -> DateTime.now)
@ -157,7 +157,7 @@ final private[simul] class SimulRepo(simulColl: Coll)(implicit ec: scala.concurr
.void
def setText(simul: Simul, text: String) =
simulColl.update
coll.update
.one(
$id(simul.id),
$set("text" -> text)
@ -165,7 +165,7 @@ final private[simul] class SimulRepo(simulColl: Coll)(implicit ec: scala.concurr
.void
def cleanup =
simulColl.delete.one(
coll.delete.one(
createdSelect ++ $doc(
"createdAt" -> $doc("$lt" -> (DateTime.now minusMinutes 60))
)

View File

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

View File

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

View File

@ -30,7 +30,7 @@ final class TeamApi(
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))

View File

@ -15,6 +15,8 @@ final class TeamRepo(val coll: Coll)(implicit ec: scala.concurrent.ExecutionCont
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 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) =
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(
userId: User.ID,
teamId: Option[TeamID] = None

View File

@ -119,8 +119,10 @@ case class User(
def addRole(role: String) = copy(roles = role :: roles)
def isVerified = roles.exists(_ contains "ROLE_VERIFIED")
def isApiHog = roles.exists(_ contains "ROLE_API_HOG")
def isVerified = roles.exists(_ contains "ROLE_VERIFIED")
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 {