send custom messages to player paired with challenge API endpoints
parent
f0df9bba47
commit
18e6b5696a
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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) { () =>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]) =
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}"/>`;
|
||||
|
||||
|
|
Loading…
Reference in New Issue