notify end of correspondence games - closes #2037

dominated-viewport
Thibault Duplessis 2016-06-22 14:45:33 +02:00
parent 924032fea0
commit 68e1195e13
9 changed files with 122 additions and 33 deletions

View File

@ -9,7 +9,7 @@ net {
ip = "5.196.91.160"
asset {
domain = ${net.domain}
version = 966
version = 967
}
}
forcedev = false
@ -410,7 +410,6 @@ gameSearch {
round {
active.ttl = 30 seconds
uid.timeout = 10 seconds
finisher.lock.timeout = 20 seconds
animation.duration = ${chessground.animation.duration}
moretime = 15 seconds
casual_only = ${app.casual_only}

View File

@ -52,9 +52,7 @@ case class Player(
def goBerserk = copy(berserk = true)
def finish(winner: Boolean) = copy(
isWinner = if (winner) Some(true) else None
)
def finish(winner: Boolean) = copy(isWinner = winner option true)
def offerDraw(turn: Int) = copy(
isOfferingDraw = true,

View File

@ -46,6 +46,11 @@ private object BSONHandlers {
implicit val BlogTitleHandler = stringAnyValHandler[NewBlogPost.Title](_.value, NewBlogPost.Title.apply)
implicit val NewBlogPostHandler = Macros.handler[NewBlogPost]
implicit val GameEndGameIdHandler = stringAnyValHandler[GameEnd.GameId](_.value, GameEnd.GameId.apply)
implicit val GameEndOpponentHandler = stringAnyValHandler[GameEnd.OpponentId](_.value, GameEnd.OpponentId.apply)
implicit val GameEndWinHandler = booleanAnyValHandler[GameEnd.Win](_.value, GameEnd.Win.apply)
implicit val GameEndHandler = Macros.handler[GameEnd]
implicit val ColorBSONHandler = new BSONHandler[BSONBoolean, chess.Color] {
def read(b: BSONBoolean) = chess.Color(b.value)
def write(c: chess.Color) = BSONBoolean(c.white)
@ -64,6 +69,7 @@ private object BSONHandlers {
case t: TeamJoined => TeamJoinedHandler.write(t)
case b: NewBlogPost => NewBlogPostHandler.write(b)
case LimitedTournamentInvitation => $empty
case x: GameEnd => GameEndHandler.write(x)
}
} ++ $doc("type" -> notificationContent.key)
@ -86,13 +92,14 @@ private object BSONHandlers {
}
def reads(reader: Reader): NotificationContent = reader.str("type") match {
case "mention" => readMentionedNotification(reader)
case "invitedStudy" => readInvitedStudyNotification(reader)
case "privateMessage" => PrivateMessageHandler read reader.doc
case "qaAnswer" => QaAnswerHandler read reader.doc
case "teamJoined" => TeamJoinedHandler read reader.doc
case "newBlogPost" => NewBlogPostHandler read reader.doc
case "u" => LimitedTournamentInvitation
case "mention" => readMentionedNotification(reader)
case "invitedStudy" => readInvitedStudyNotification(reader)
case "privateMessage" => PrivateMessageHandler read reader.doc
case "qaAnswer" => QaAnswerHandler read reader.doc
case "teamJoined" => TeamJoinedHandler read reader.doc
case "newBlogPost" => NewBlogPostHandler read reader.doc
case "u" => LimitedTournamentInvitation
case "gameEnd" => GameEndHandler read reader.doc
}
def writes(writer: Writer, n: NotificationContent): dsl.Bdoc = {

View File

@ -37,6 +37,10 @@ final class JSONHandlers(
"slug" -> slug.value,
"title" -> title.value)
case LimitedTournamentInvitation => Json.obj()
case GameEnd(gameId, opponentId, win) => Json.obj(
"id" -> gameId.value,
"opponent" -> opponentId.map(_.value).flatMap(getLightUser),
"win" -> win.map(_.value))
}
}

View File

@ -102,3 +102,14 @@ object NewBlogPost {
}
case object LimitedTournamentInvitation extends NotificationContent("u")
case class GameEnd(
gameId: GameEnd.GameId,
opponentId: Option[GameEnd.OpponentId],
win: Option[GameEnd.Win]) extends NotificationContent("gameEnd")
object GameEnd {
case class GameId(value: String) extends AnyVal
case class OpponentId(value: String) extends AnyVal
case class Win(value: Boolean) extends AnyVal
}

View File

@ -7,6 +7,8 @@ import scala.concurrent.duration._
import actorApi.{ GetSocketStatus, SocketStatus }
import lila.common.PimpedConfig._
import lila.game.{ Game, Pov }
import lila.hub.actorApi.HasUserId
import lila.hub.actorApi.map.{ Ask, Tell }
import lila.socket.actorApi.GetVersion
import makeTimeout.large
@ -23,6 +25,7 @@ final class Env(
lightUser: String => Option[lila.common.LightUser],
userJsonView: lila.user.JsonView,
rankingApi: lila.user.RankingApi,
notifyApi: lila.notify.NotifyApi,
uciMemo: lila.game.UciMemo,
rematch960Cache: lila.memo.ExpireSetMemo,
isRematchCache: lila.memo.ExpireSetMemo,
@ -41,7 +44,6 @@ final class Env(
val Moretime = config duration "moretime"
val SocketName = config getString "socket.name"
val SocketTimeout = config duration "socket.timeout"
val FinisherLockTimeout = config duration "finisher.lock.timeout"
val NetDomain = config getString "net.domain"
val ActorMapName = config getString "actor.map.name"
val CasualOnly = config getBoolean "casual_only"
@ -126,13 +128,18 @@ final class Env(
coll = db(CollectionForecast),
roundMap = hub.actor.roundMap)
private lazy val notifier = new RoundNotifier(
timeline = hub.actor.timeline,
isUserPresent = isUserPresent,
notifyApi = notifyApi)
private lazy val finisher = new Finisher(
messenger = messenger,
perfsUpdater = perfsUpdater,
crosstableApi = crosstableApi,
notifier = notifier,
playban = playban,
bus = system.lilaBus,
timeline = hub.actor.timeline,
casualOnly = CasualOnly)
private lazy val rematcher = new Rematcher(
@ -165,6 +172,9 @@ final class Env(
private def getSocketStatus(gameId: String): Fu[SocketStatus] =
socketHub ? Ask(gameId, GetSocketStatus) mapTo manifest[SocketStatus]
private def isUserPresent(game: Game, userId: lila.user.User.ID): Fu[Boolean] =
socketHub ? Ask(game.id, HasUserId(userId)) mapTo manifest[Boolean]
lazy val jsonView = new JsonView(
noteApi = noteApi,
userJsonView = userJsonView,
@ -192,12 +202,12 @@ final class Env(
val tvBroadcast = system.actorOf(Props(classOf[TvBroadcast]))
system.lilaBus.subscribe(tvBroadcast, 'moveEvent, 'changeFeaturedGame)
def checkOutoftime(game: lila.game.Game) {
def checkOutoftime(game: Game) {
if (game.playable && game.started && !game.isUnlimited)
roundMap ! Tell(game.id, actorApi.round.Outoftime)
}
def resign(pov: lila.game.Pov) {
def resign(pov: Pov) {
if (pov.game.abortable)
roundMap ! Tell(pov.game.id, actorApi.round.Abort(pov.playerId))
else if (pov.game.playable)
@ -219,6 +229,7 @@ object Env {
lightUser = lila.user.Env.current.lightUser,
userJsonView = lila.user.Env.current.jsonView,
rankingApi = lila.user.Env.current.rankingApi,
notifyApi = lila.notify.Env.current.api,
uciMemo = lila.game.Env.current.uciMemo,
rematch960Cache = lila.game.Env.current.cached.rematch960,
isRematchCache = lila.game.Env.current.cached.isRematch,

View File

@ -15,9 +15,9 @@ private[round] final class Finisher(
messenger: Messenger,
perfsUpdater: PerfsUpdater,
playban: PlaybanApi,
notifier: RoundNotifier,
crosstableApi: lila.game.CrosstableApi,
bus: lila.common.Bus,
timeline: akka.actor.ActorSelection,
casualOnly: Boolean) {
def abort(pov: Pov)(implicit proxy: GameProxy): Fu[Events] = apply(pov.game, _.Aborted) >>- {
@ -56,8 +56,7 @@ private[round] final class Finisher(
message: Option[SelectI18nKey] = None)(implicit proxy: GameProxy): Fu[Events] = {
val status = makeStatus(Status)
val prog = game.finish(status, winner)
if (game.nonAi && game.isCorrespondence)
Color.all foreach notifyTimeline(prog.game)
if (game.nonAi && game.isCorrespondence) Color.all foreach notifier.gameEnd(prog.game)
lila.mon.game.finish(status.name)()
casualOnly.fold(
GameRepo unrate prog.game.id inject prog.game.copy(mode = chess.Mode.Casual),
@ -86,19 +85,6 @@ private[round] final class Finisher(
}
} >>- proxy.invalidate
private def notifyTimeline(game: Game)(color: Color) = {
import lila.hub.actorApi.timeline.{ Propagate, GameEnd }
if (!game.aborted) game.player(color).userId foreach { userId =>
game.perfType foreach { perfType =>
timeline ! (Propagate(GameEnd(
playerId = game fullIdOf color,
opponent = game.player(!color).userId,
win = game.winnerColor map (color ==),
perf = perfType.key)) toUser userId)
}
}
}
private def updateCountAndPerfs(finish: FinishGame): Funit =
(!finish.isVsSelf && !finish.game.aborted) ?? {
(finish.white |@| finish.black).tupled ?? {

View File

@ -0,0 +1,34 @@
package lila.round
import lila.hub.actorApi.timeline.{ Propagate, GameEnd => TLGameEnd }
import lila.notify.{ GameEnd, Notification, NotifyApi }
import lila.game.Game
import lila.user.User
private final class RoundNotifier(
timeline: akka.actor.ActorSelection,
isUserPresent: (Game, User.ID) => Fu[Boolean],
notifyApi: NotifyApi) {
def gameEnd(game: Game)(color: chess.Color) = {
if (!game.aborted) game.player(color).userId foreach { userId =>
game.perfType foreach { perfType =>
timeline ! (Propagate(TLGameEnd(
playerId = game fullIdOf color,
opponent = game.player(!color).userId,
win = game.winnerColor map (color ==),
perf = perfType.key)) toUser userId)
}
isUserPresent(game, userId) foreach {
case false => notifyApi.addNotification(Notification(
Notification.Notifies(userId),
GameEnd(
GameEnd.GameId(game.id),
game.opponent(color).userId map GameEnd.OpponentId.apply,
game.wonBy(color) map GameEnd.Win.apply)))
case _ =>
}
}
}
}

View File

@ -1,7 +1,7 @@
var m = require('mithril');
function userFullName(u) {
if (!u) return '?';
if (!u) return 'Anonymous';
return u.title ? u.title + ' ' + u.name : u.name;
}
@ -145,6 +145,45 @@ var handlers = {
text: function(n) {
return 'Game with ' + n.content.opponentName + '.';
}
},
gameEnd: {
html: function(notification) {
var content = notification.content
var url = "/" + content.id;
var result;
switch (content.win) {
case true:
result = 'Congratulations, you won!';
break;
case false:
result = 'You lost!';
break;
default:
result = "It's a draw.";
}
return genericNotification(notification, url, ';', [
m('span', [
m('strong', 'Game vs ' + userFullName(content.opponent)),
drawTime(notification)
]),
m('span', result)
]);
},
text: function(n) {
var result;
switch (n.content.win) {
case true:
result = 'Victory';
break;
case false:
result = 'Defeat';
break;
default:
result = 'Draw';
}
return result + ' vs ' + userFullName(n.content.opponent);
}
}
};