send custom messages to player paired with challenge API endpoints

pull/8123/head
Thibault Duplessis 2021-02-08 21:03:31 +01:00
parent f0df9bba47
commit 18e6b5696a
14 changed files with 124 additions and 42 deletions

View File

@ -15,6 +15,7 @@ import lila.oauth.{ AccessToken, OAuthScope }
import lila.setup.ApiConfig
import lila.socket.Socket.SocketVersion
import lila.user.{ User => UserModel }
import lila.common.Template
final class Challenge(
env: Env
@ -285,7 +286,8 @@ final class Challenge(
env.user.repo enabledById userId.toLowerCase flatMap { destUser =>
val challenge = makeOauthChallenge(config, me, destUser)
(destUser, config.acceptByToken) match {
case (Some(dest), Some(strToken)) => apiChallengeAccept(dest, challenge, strToken)
case (Some(dest), Some(strToken)) =>
apiChallengeAccept(dest, challenge, strToken)(me, config.message)
case _ =>
destUser ?? { env.challenge.granter(me.some, _, config.perfType) } flatMap {
case Some(denied) =>
@ -318,7 +320,11 @@ final class Challenge(
.bindFromRequest()
.fold(
err => BadRequest(apiFormError(err)).fuccess,
config => acceptOauthChallenge(dest, makeOauthChallenge(config, orig, dest.some))
config =>
acceptOauthChallenge(dest, makeOauthChallenge(config, orig, dest.some))(
admin,
config.message
)
)
}
}
@ -345,11 +351,14 @@ final class Challenge(
)
}
private def acceptOauthChallenge(dest: UserModel, challenge: ChallengeModel) =
env.challenge.api.oauthAccept(dest, challenge) map {
case None => BadRequest(jsonError("Couldn't create game"))
private def acceptOauthChallenge(
dest: UserModel,
challenge: ChallengeModel
)(managedBy: lila.user.User, template: Option[Template]) =
env.challenge.api.oauthAccept(dest, challenge) flatMap {
case None => BadRequest(jsonError("Couldn't create game")).fuccess
case Some(g) =>
Ok(
env.challenge.msg.onApiPair(challenge)(managedBy, template) inject Ok(
Json.obj(
"game" -> {
env.game.jsonView(g, challenge.initialFen) ++ Json.obj(
@ -364,7 +373,7 @@ final class Challenge(
dest: UserModel,
challenge: lila.challenge.Challenge,
strToken: String
) =
)(managedBy: lila.user.User, message: Option[Template]) =
env.security.api.oauthScoped(
lila.oauth.AccessToken.Id(strToken),
List(lila.oauth.OAuthScope.Challenge.Write)
@ -372,7 +381,7 @@ final class Challenge(
_.fold(
err => BadRequest(jsonError(err.message)).fuccess,
scoped =>
if (scoped.user is dest) acceptOauthChallenge(dest, challenge)
if (scoped.user is dest) acceptOauthChallenge(dest, challenge)(managedBy, message)
else BadRequest(jsonError("dest and accept user don't match")).fuccess
)
}

View File

@ -280,7 +280,7 @@ lazy val shutup = module("shutup",
)
lazy val challenge = module("challenge",
Seq(common, db, hub, setup, game, relation, pref, socket, room),
Seq(common, db, hub, setup, game, relation, pref, socket, room, msg),
Seq(scalatags, lettuce, specs2) ++ reactivemongo.bundle
)

View File

@ -10,6 +10,7 @@ import scala.concurrent.duration._
import lila.common.Bus
import lila.common.LilaStream
import lila.common.Template
import lila.db.dsl._
import lila.game.{ Game, Player }
import lila.hub.actorApi.map.TellMany
@ -20,6 +21,7 @@ import lila.user.User
final class ChallengeBulkApi(
colls: ChallengeColls,
msgApi: ChallengeMsg,
gameRepo: lila.game.GameRepo,
userRepo: lila.user.UserRepo,
onStart: lila.round.OnStart
@ -33,6 +35,7 @@ final class ChallengeBulkApi(
implicit private val gameHandler = Macros.handler[ScheduledGame]
implicit private val variantHandler = variantByKeyHandler
implicit private val clockHandler = clockConfigHandler
implicit private val messageHandler = stringAnyValHandler[Template](_.value, Template.apply)
implicit private val bulkHandler = Macros.handler[ScheduledBulk]
private val coll = colls.bulk
@ -92,8 +95,8 @@ final class ChallengeBulkApi(
}
}
.mapConcat(_.toList)
.map[Game] { case (id, white, black) =>
Game
.map { case (id, white, black) =>
val game = Game
.make(
chess = chess.Game(situation = Situation(bulk.variant), clock = bulk.clock.toClock.some),
whitePlayer = Player.make(chess.White, white.some, _(perfType)),
@ -104,9 +107,15 @@ final class ChallengeBulkApi(
)
.withId(id)
.start
(game, white, black)
}
.mapAsyncUnordered(8) { game =>
(gameRepo insertDenormalized game) >>- onStart(game.id)
.mapAsyncUnordered(8) { case (game, white, black) =>
gameRepo.insertDenormalized(game) >>- onStart(game.id) inject {
(game, white, black)
}
}
.mapAsyncUnordered(8) { case (game, white, black) =>
msgApi.onApiPair(game.id, white.light, black.light)(bulk.by, bulk.message)
}
.toMat(LilaStream.sinkCount)(Keep.right)
.run()

View File

@ -0,0 +1,34 @@
package lila.challenge
import scala.concurrent.ExecutionContext
import lila.common.{ LightUser, Template }
import lila.user.{ LightUserApi, User }
final class ChallengeMsg(msgApi: lila.msg.MsgApi, lightUserApi: LightUserApi)(implicit
ec: ExecutionContext
) {
def onApiPair(challenge: Challenge)(managedBy: User, template: Option[Template]): Funit =
challenge.userIds.map(lightUserApi.async).sequenceFu.flatMap {
_.flatten match {
case List(u1, u2) => onApiPair(challenge.id, u1, u2)(managedBy.id, template)
case _ => funit
}
}
def onApiPair(gameId: lila.game.Game.ID, u1: LightUser, u2: LightUser)(
managedById: User.ID,
template: Option[Template]
): Funit =
List(u1 -> u2, u2 -> u1)
.map { case (u1, u2) =>
val msg = template
.fold("Your game with {opponent} is ready: {game}.")(_.value)
.replace("{opponent}", s"@${u2.name}")
.replace("{game}", s"#${gameId}")
msgApi.post(managedById, u1.id, msg, multi = true)
}
.sequenceFu
.void
}

View File

@ -15,12 +15,14 @@ final class Env(
onStart: lila.round.OnStart,
gameCache: lila.game.Cached,
lightUser: lila.common.LightUser.GetterSync,
lightUserApi: lila.user.LightUserApi,
isOnline: lila.socket.IsOnline,
db: lila.db.Db,
cacheApi: lila.memo.CacheApi,
prefApi: lila.pref.PrefApi,
relationApi: lila.relation.RelationApi,
remoteSocketApi: lila.socket.RemoteSocket,
msgApi: lila.msg.MsgApi,
baseUrl: BaseUrl
)(implicit
ec: scala.concurrent.ExecutionContext,
@ -51,6 +53,8 @@ final class Env(
lazy val bulk = wire[ChallengeBulkApi]
lazy val msg = wire[ChallengeMsg]
val forms = new ChallengeForm
system.scheduler.scheduleWithFixedDelay(10 seconds, 3343 millis) { () =>

View File

@ -130,3 +130,5 @@ case class Ints(value: List[Int]) extends AnyVal
case class Every(value: FiniteDuration) extends AnyVal
case class AtMost(value: FiniteDuration) extends AnyVal
case class Template(value: String) extends AnyVal

View File

@ -1,9 +1,13 @@
package lila.msg
import lila.common.config.BaseUrl
case class MsgPreset(name: String, text: String)
object MsgPreset {
type Username = String
lazy val sandbagAuto = MsgPreset(
name = "Warning: possible sandbagging",
text =
@ -20,7 +24,7 @@ Thank you for your understanding."""
This can be very annoying for your opponents. If this behavior continues to happen, we may be forced to terminate your account."""
)
def maxFollow(username: String, max: Int) =
def maxFollow(username: Username, max: Int) =
MsgPreset(
name = "Follow limit reached!",
text = s"""Sorry, you can't follow more than $max players on Lichess.

View File

@ -20,8 +20,6 @@ final case class ApiAiConfig(
val strictFen = false
def >> = (level, variant.key.some, clock, daysO, color.name.some, fen.map(_.value)).some
val days = ~daysO
val increment = clock.??(_.increment.roundSeconds)
val time = clock.??(_.limit.roundSeconds / 60)

View File

@ -9,6 +9,7 @@ import lila.game.PerfPicker
import lila.lobby.Color
import lila.rating.PerfType
import chess.variant.Variant
import lila.common.Template
final case class ApiConfig(
variant: chess.variant.Variant,
@ -17,11 +18,10 @@ final case class ApiConfig(
rated: Boolean,
color: Color,
position: Option[FEN] = None,
acceptByToken: Option[String] = None
acceptByToken: Option[String] = None,
message: Option[Template]
) {
def >> = (variant.key.some, clock, days, rated, color.name.some, position.map(_.value), acceptByToken).some
def perfType: Option[PerfType] = PerfPicker.perfType(chess.Speed(clock), variant, days)
def validFen = ApiConfig.validFen(variant, position)
@ -49,7 +49,8 @@ object ApiConfig extends BaseHumanConfig {
r: Boolean,
c: Option[String],
pos: Option[String],
tok: Option[String]
tok: Option[String],
msg: Option[String]
) =
new ApiConfig(
variant = chess.variant.Variant.orDefault(~v),
@ -58,7 +59,8 @@ object ApiConfig extends BaseHumanConfig {
rated = r,
color = Color.orDefault(~c),
position = pos map FEN.apply,
acceptByToken = tok
acceptByToken = tok,
message = msg map Template
).autoVariant
def validFen(variant: Variant, fen: Option[FEN]) =

View File

@ -13,8 +13,6 @@ final case class OpenConfig(
position: Option[FEN] = None
) {
def >> = (variant.key.some, clock, position.map(_.value)).some
def perfType: Option[PerfType] = PerfPicker.perfType(chess.Speed(clock), variant, none)
def validFen = ApiConfig.validFen(variant, position)

View File

@ -16,6 +16,7 @@ import lila.oauth.AccessToken
import lila.oauth.OAuthScope
import lila.oauth.OAuthServer
import lila.user.User
import lila.common.Template
object SetupBulk {
@ -27,7 +28,8 @@ object SetupBulk {
clock: Clock.Config,
rated: Boolean,
pairAt: Option[DateTime],
startClocksAt: Option[DateTime]
startClocksAt: Option[DateTime],
message: Option[Template]
)
private def timestampInNearFuture = longNumber(
@ -51,7 +53,8 @@ object SetupBulk {
"clock" -> SetupForm.api.clockMapping,
"rated" -> boolean,
"pairAt" -> optional(timestampInNearFuture),
"startClocksAt" -> optional(timestampInNearFuture)
"startClocksAt" -> optional(timestampInNearFuture),
"message" -> SetupForm.api.message
) {
(
tokens: String,
@ -59,7 +62,8 @@ object SetupBulk {
clock: Clock.Config,
rated: Boolean,
pairTs: Option[Long],
clockTs: Option[Long]
clockTs: Option[Long],
message: Option[String]
) =>
BulkFormData(
tokens,
@ -67,7 +71,8 @@ object SetupBulk {
clock,
rated,
pairTs.map { new DateTime(_) },
clockTs.map { new DateTime(_) }
clockTs.map { new DateTime(_) },
message map Template
)
}(_ => None)
)
@ -99,6 +104,7 @@ object SetupBulk {
pairAt: DateTime,
startClocksAt: Option[DateTime],
scheduledAt: DateTime,
message: Option[Template],
pairedAt: Option[DateTime] = None
) {
def userSet = Set(games.flatMap(g => List(g.white, g.black)))
@ -210,6 +216,7 @@ final class SetupBulkApi(oauthServer: OAuthServer, idGenerator: IdGenerator)(imp
Mode(data.rated),
pairAt = data.pairAt | DateTime.now,
startClocksAt = data.startClocksAt,
message = data.message,
scheduledAt = DateTime.now
)
}

View File

@ -115,6 +115,13 @@ object SetupForm {
lazy val variant =
"variant" -> optional(text.verifying(Variant.byKey.contains _))
lazy val message = optional(
nonEmptyText.verifying(
"The message must contain {game}, which will be replaced with the game URL.",
_ contains "{game}"
)
)
def user(from: User) =
Form(challengeMapping.verifying("Invalid speed", _ validSpeed from.isBot))
@ -128,8 +135,9 @@ object SetupForm {
"rated" -> boolean,
"color" -> optional(color),
"fen" -> fenField,
"acceptByToken" -> optional(nonEmptyText)
)(ApiConfig.from)(_.>>)
"acceptByToken" -> optional(nonEmptyText),
"message" -> message
)(ApiConfig.from)(_ => none)
.verifying("invalidFen", _.validFen)
lazy val ai = Form(
@ -140,7 +148,7 @@ object SetupForm {
"days" -> optional(days),
"color" -> optional(color),
"fen" -> fenField
)(ApiAiConfig.from)(_.>>).verifying("invalidFen", _.validFen)
)(ApiAiConfig.from)(_ => none).verifying("invalidFen", _.validFen)
)
lazy val open = Form(
@ -148,7 +156,7 @@ object SetupForm {
variant,
clock,
"fen" -> fenField
)(OpenConfig.from)(_.>>).verifying("invalidFen", _.validFen)
)(OpenConfig.from)(_ => none).verifying("invalidFen", _.validFen)
)
}
}

View File

@ -1,11 +1,11 @@
import { h } from 'snabbdom';
import { VNode } from 'snabbdom/vnode';
import { Convo } from '../interfaces';
import { userName, bindMobileMousedown } from './util';
import renderMsgs from './msgs';
import MsgCtrl from '../ctrl';
import renderActions from './actions';
import renderInteract from './interact';
import MsgCtrl from '../ctrl';
import renderMsgs from './msgs';
import { Convo } from '../interfaces';
import { h } from 'snabbdom';
import { userName, bindMobileMousedown } from './util';
import { VNode } from 'snabbdom/vnode';
export default function renderConvo(ctrl: MsgCtrl, convo: Convo): VNode {
const user = convo.user;

View File

@ -1,15 +1,22 @@
import { scroller } from './scroller';
// looks like it has a @mention or a url.tld
export const isMoreThanText = (str: string) => /(\n|(@|\.)\w{2,})/.test(str);
// looks like it has a @mention or #gameid or a url.tld
export const isMoreThanText = (str: string) => /(\n|(@|#|\.)\w{2,})/.test(str);
export const enhance = (str: string) => expandMentions(expandUrls(lichess.escapeHtml(str))).replace(/\n/g, '<br>');
export const enhance = (str: string) =>
expandGameIds(expandMentions(expandUrls(lichess.escapeHtml(str)))).replace(/\n/g, '<br>');
const expandMentions = (html: string) =>
html.replace(/(^|[^\w@#/])@([\w-]{2,})/g, (orig: string, prefix: string, user: string) =>
user.length > 20 ? orig : `${prefix}${a('/@/' + user, '@' + user)}`
);
const expandGameIds = (html: string) =>
html.replace(
/\s#([\w]{8})($|[^\w-])/g,
(_: string, id: string, suffix: string) => ' ' + a('/' + id, '#' + id, 'text') + suffix
);
// ported from https://github.com/bryanwoods/autolink-js/blob/master/autolink.js
const urlRegex = /(^|[\s\n]|<[A-Za-z]*\/?>)((?:(?:https?|ftp):\/\/|lichess\.org)[\-A-Z0-9+\u0026\u2019@#\/%?=()~_|!:,.;]*[\-A-Z0-9+\u0026@#\/%=~()_|])/gi;
const expandUrls = (html: string) =>
@ -31,10 +38,10 @@ const expandImage = (url: string) => (/\.(jpg|jpeg|png|gif)$/.test(url) ? aImg(u
const expandLink = (url: string) => a(url, url.replace(/^https?:\/\//, ''));
const a = (href: string, body: string) =>
const a = (href: string, body: string, cls?: string) =>
`<a target="_blank" rel="noopener nofollow" href="${
href.startsWith('/') || href.includes('://') ? href : '//' + href
}">${body}</a>`;
}"${cls ? ` class="${cls}"` : ''}>${body}</a>`;
const img = (src: string) => `<img src="${src}"/>`;