2013-03-22 11:53:13 -06:00
|
|
|
package lila.game
|
2012-05-14 14:36:32 -06:00
|
|
|
|
|
|
|
import play.api.libs.json._
|
|
|
|
|
2016-01-15 07:39:35 -07:00
|
|
|
import chess.variant.Crazyhouse
|
2019-12-13 07:30:20 -07:00
|
|
|
import chess.{
|
|
|
|
Centis,
|
|
|
|
PromotableRole,
|
|
|
|
Pos,
|
|
|
|
Color,
|
|
|
|
Situation,
|
|
|
|
Move => ChessMove,
|
|
|
|
Drop => ChessDrop,
|
|
|
|
Clock => ChessClock,
|
|
|
|
Status
|
|
|
|
}
|
2016-01-15 07:39:35 -07:00
|
|
|
import JsonView._
|
2019-12-13 07:30:20 -07:00
|
|
|
import lila.chat.{ PlayerLine, UserLine }
|
2018-12-15 07:09:13 -07:00
|
|
|
import lila.common.ApiVersion
|
2021-11-20 01:51:26 -07:00
|
|
|
import lila.common.Json._
|
2012-05-14 14:36:32 -06:00
|
|
|
|
|
|
|
sealed trait Event {
|
|
|
|
def typ: String
|
|
|
|
def data: JsValue
|
2019-12-13 07:30:20 -07:00
|
|
|
def only: Option[Color] = None
|
|
|
|
def owner: Boolean = false
|
|
|
|
def watcher: Boolean = false
|
|
|
|
def troll: Boolean = false
|
2018-12-15 06:28:25 -07:00
|
|
|
def moveBy: Option[Color] = None
|
2012-05-14 14:36:32 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
object Event {
|
|
|
|
|
|
|
|
sealed trait Empty extends Event {
|
|
|
|
def data = JsNull
|
|
|
|
}
|
|
|
|
|
2013-03-30 15:30:47 -06:00
|
|
|
object Start extends Empty {
|
2012-05-14 14:36:32 -06:00
|
|
|
def typ = "start"
|
|
|
|
}
|
|
|
|
|
2016-01-18 00:21:52 -07:00
|
|
|
object MoveOrDrop {
|
|
|
|
|
|
|
|
def data(
|
2019-12-13 07:30:20 -07:00
|
|
|
fen: String,
|
|
|
|
check: Boolean,
|
|
|
|
threefold: Boolean,
|
|
|
|
state: State,
|
|
|
|
clock: Option[ClockEvent],
|
|
|
|
possibleMoves: Map[Pos, List[Pos]],
|
|
|
|
possibleDrops: Option[List[Pos]],
|
|
|
|
crazyData: Option[Crazyhouse.Data]
|
2017-02-14 08:34:07 -07:00
|
|
|
)(extra: JsObject) = {
|
2019-12-13 07:30:20 -07:00
|
|
|
extra ++ Json
|
|
|
|
.obj(
|
|
|
|
"fen" -> fen,
|
|
|
|
"ply" -> state.turns,
|
|
|
|
"dests" -> PossibleMoves.oldJson(possibleMoves)
|
|
|
|
)
|
|
|
|
.add("clock" -> clock.map(_.data))
|
2017-07-08 05:58:31 -06:00
|
|
|
.add("status" -> state.status)
|
|
|
|
.add("winner" -> state.winner)
|
|
|
|
.add("check" -> check)
|
|
|
|
.add("threefold" -> threefold)
|
|
|
|
.add("wDraw" -> state.whiteOffersDraw)
|
|
|
|
.add("bDraw" -> state.blackOffersDraw)
|
|
|
|
.add("crazyhouse" -> crazyData)
|
|
|
|
.add("drops" -> possibleDrops.map { squares =>
|
|
|
|
JsString(squares.map(_.key).mkString)
|
|
|
|
})
|
|
|
|
}
|
2016-01-18 00:21:52 -07:00
|
|
|
}
|
2016-01-15 07:39:35 -07:00
|
|
|
|
2015-04-27 14:36:43 -06:00
|
|
|
case class Move(
|
|
|
|
orig: Pos,
|
|
|
|
dest: Pos,
|
|
|
|
san: String,
|
2020-10-18 12:21:34 -06:00
|
|
|
fen: String, // not a FEN, just a board fen
|
2015-06-15 17:26:39 -06:00
|
|
|
check: Boolean,
|
|
|
|
threefold: Boolean,
|
|
|
|
promotion: Option[Promotion],
|
|
|
|
enpassant: Option[Enpassant],
|
2015-06-15 17:56:27 -06:00
|
|
|
castle: Option[Castling],
|
2015-06-15 18:09:38 -06:00
|
|
|
state: State,
|
2017-07-08 06:49:10 -06:00
|
|
|
clock: Option[ClockEvent],
|
2016-01-15 07:39:35 -07:00
|
|
|
possibleMoves: Map[Pos, List[Pos]],
|
2016-01-18 00:21:52 -07:00
|
|
|
possibleDrops: Option[List[Pos]],
|
2017-02-14 08:34:07 -07:00
|
|
|
crazyData: Option[Crazyhouse.Data]
|
|
|
|
) extends Event {
|
2012-05-14 14:36:32 -06:00
|
|
|
def typ = "move"
|
2020-05-05 22:11:15 -06:00
|
|
|
def data =
|
|
|
|
MoveOrDrop.data(fen, check, threefold, state, clock, possibleMoves, possibleDrops, crazyData) {
|
|
|
|
Json
|
|
|
|
.obj(
|
|
|
|
"uci" -> s"${orig.key}${dest.key}",
|
|
|
|
"san" -> san
|
|
|
|
)
|
|
|
|
.add("promotion" -> promotion.map(_.data))
|
|
|
|
.add("enpassant" -> enpassant.map(_.data))
|
|
|
|
.add("castle" -> castle.map(_.data))
|
|
|
|
}
|
2018-12-15 06:28:25 -07:00
|
|
|
override def moveBy = Some(!state.color)
|
2012-05-14 14:36:32 -06:00
|
|
|
}
|
|
|
|
object Move {
|
2019-12-13 07:30:20 -07:00
|
|
|
def apply(
|
|
|
|
move: ChessMove,
|
|
|
|
situation: Situation,
|
|
|
|
state: State,
|
|
|
|
clock: Option[ClockEvent],
|
|
|
|
crazyData: Option[Crazyhouse.Data]
|
2020-05-05 22:11:15 -06:00
|
|
|
): Move =
|
|
|
|
Move(
|
|
|
|
orig = move.orig,
|
|
|
|
dest = move.dest,
|
|
|
|
san = chess.format.pgn.Dumper(move),
|
|
|
|
fen = chess.format.Forsyth.exportBoard(situation.board),
|
|
|
|
check = situation.check,
|
|
|
|
threefold = situation.threefoldRepetition,
|
|
|
|
promotion = move.promotion.map { Promotion(_, move.dest) },
|
|
|
|
enpassant = (move.capture ifTrue move.enpassant).map {
|
|
|
|
Event.Enpassant(_, !move.color)
|
|
|
|
},
|
2020-09-21 01:28:28 -06:00
|
|
|
castle = move.castle.map { case (king, rook) =>
|
|
|
|
Castling(king, rook, move.color)
|
2020-05-05 22:11:15 -06:00
|
|
|
},
|
|
|
|
state = state,
|
|
|
|
clock = clock,
|
|
|
|
possibleMoves = situation.destinations,
|
|
|
|
possibleDrops = situation.drops,
|
|
|
|
crazyData = crazyData
|
|
|
|
)
|
2012-05-14 14:36:32 -06:00
|
|
|
}
|
|
|
|
|
2016-01-15 06:21:16 -07:00
|
|
|
case class Drop(
|
|
|
|
role: chess.Role,
|
|
|
|
pos: Pos,
|
|
|
|
san: String,
|
|
|
|
fen: String,
|
|
|
|
check: Boolean,
|
|
|
|
threefold: Boolean,
|
|
|
|
state: State,
|
2017-07-08 06:49:10 -06:00
|
|
|
clock: Option[ClockEvent],
|
2016-01-15 07:39:35 -07:00
|
|
|
possibleMoves: Map[Pos, List[Pos]],
|
2016-01-18 00:21:52 -07:00
|
|
|
crazyData: Option[Crazyhouse.Data],
|
2017-02-14 08:34:07 -07:00
|
|
|
possibleDrops: Option[List[Pos]]
|
|
|
|
) extends Event {
|
2016-01-15 06:21:16 -07:00
|
|
|
def typ = "drop"
|
2020-05-05 22:11:15 -06:00
|
|
|
def data =
|
|
|
|
MoveOrDrop.data(fen, check, threefold, state, clock, possibleMoves, possibleDrops, crazyData) {
|
|
|
|
Json.obj(
|
|
|
|
"role" -> role.name,
|
|
|
|
"uci" -> s"${role.pgn}@${pos.key}",
|
|
|
|
"san" -> san
|
|
|
|
)
|
|
|
|
}
|
2018-12-15 06:28:25 -07:00
|
|
|
override def moveBy = Some(!state.color)
|
2016-01-15 06:21:16 -07:00
|
|
|
}
|
|
|
|
object Drop {
|
2019-12-13 07:30:20 -07:00
|
|
|
def apply(
|
|
|
|
drop: ChessDrop,
|
|
|
|
situation: Situation,
|
|
|
|
state: State,
|
|
|
|
clock: Option[ClockEvent],
|
|
|
|
crazyData: Option[Crazyhouse.Data]
|
2020-05-05 22:11:15 -06:00
|
|
|
): Drop =
|
|
|
|
Drop(
|
|
|
|
role = drop.piece.role,
|
|
|
|
pos = drop.pos,
|
|
|
|
san = chess.format.pgn.Dumper(drop),
|
|
|
|
fen = chess.format.Forsyth.exportBoard(situation.board),
|
|
|
|
check = situation.check,
|
|
|
|
threefold = situation.threefoldRepetition,
|
|
|
|
state = state,
|
|
|
|
clock = clock,
|
|
|
|
possibleMoves = situation.destinations,
|
|
|
|
possibleDrops = situation.drops,
|
|
|
|
crazyData = crazyData
|
|
|
|
)
|
2016-01-15 06:21:16 -07:00
|
|
|
}
|
|
|
|
|
2015-06-16 04:09:36 -06:00
|
|
|
object PossibleMoves {
|
2018-12-15 07:09:13 -07:00
|
|
|
|
|
|
|
def json(moves: Map[Pos, List[Pos]], apiVersion: ApiVersion) =
|
|
|
|
if (apiVersion gte 4) newJson(moves)
|
|
|
|
else oldJson(moves)
|
|
|
|
|
|
|
|
def newJson(moves: Map[Pos, List[Pos]]) =
|
|
|
|
if (moves.isEmpty) JsNull
|
|
|
|
else {
|
2019-12-13 07:30:20 -07:00
|
|
|
val sb = new java.lang.StringBuilder(128)
|
2018-12-15 07:09:13 -07:00
|
|
|
var first = true
|
2020-09-21 01:28:28 -06:00
|
|
|
moves foreach { case (orig, dests) =>
|
|
|
|
if (first) first = false
|
|
|
|
else sb append " "
|
|
|
|
sb append orig.key
|
|
|
|
dests foreach { sb append _.key }
|
2018-12-15 07:09:13 -07:00
|
|
|
}
|
|
|
|
JsString(sb.toString)
|
|
|
|
}
|
|
|
|
|
|
|
|
def oldJson(moves: Map[Pos, List[Pos]]) =
|
2015-06-16 04:09:36 -06:00
|
|
|
if (moves.isEmpty) JsNull
|
2019-12-13 07:30:20 -07:00
|
|
|
else
|
2020-09-21 01:28:28 -06:00
|
|
|
moves.foldLeft(JsObject(Nil)) { case (res, (o, d)) =>
|
|
|
|
res + (o.key -> JsString(d map (_.key) mkString))
|
2019-12-13 07:30:20 -07:00
|
|
|
}
|
2015-06-16 04:09:36 -06:00
|
|
|
}
|
|
|
|
|
2015-01-11 16:56:03 -07:00
|
|
|
case class Enpassant(pos: Pos, color: Color) extends Event {
|
2012-05-14 14:36:32 -06:00
|
|
|
def typ = "enpassant"
|
2020-05-05 22:11:15 -06:00
|
|
|
def data =
|
|
|
|
Json.obj(
|
|
|
|
"key" -> pos.key,
|
|
|
|
"color" -> color
|
|
|
|
)
|
2012-05-14 14:36:32 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
case class Castling(king: (Pos, Pos), rook: (Pos, Pos), color: Color) extends Event {
|
|
|
|
def typ = "castling"
|
2020-05-05 22:11:15 -06:00
|
|
|
def data =
|
|
|
|
Json.obj(
|
|
|
|
"king" -> Json.arr(king._1.key, king._2.key),
|
|
|
|
"rook" -> Json.arr(rook._1.key, rook._2.key),
|
|
|
|
"color" -> color
|
|
|
|
)
|
2012-05-14 14:36:32 -06:00
|
|
|
}
|
|
|
|
|
2014-02-12 16:23:18 -07:00
|
|
|
case class RedirectOwner(
|
|
|
|
color: Color,
|
2014-08-13 16:14:03 -06:00
|
|
|
id: String,
|
2017-02-14 08:34:07 -07:00
|
|
|
cookie: Option[JsObject]
|
|
|
|
) extends Event {
|
2012-05-14 14:36:32 -06:00
|
|
|
def typ = "redirect"
|
2019-12-13 07:30:20 -07:00
|
|
|
def data =
|
|
|
|
Json
|
|
|
|
.obj(
|
|
|
|
"id" -> id,
|
|
|
|
"url" -> s"/$id"
|
|
|
|
)
|
|
|
|
.add("cookie" -> cookie)
|
2012-05-14 14:36:32 -06:00
|
|
|
override def only = Some(color)
|
|
|
|
}
|
|
|
|
|
|
|
|
case class Promotion(role: PromotableRole, pos: Pos) extends Event {
|
|
|
|
def typ = "promotion"
|
2020-05-05 22:11:15 -06:00
|
|
|
def data =
|
|
|
|
Json.obj(
|
|
|
|
"key" -> pos.key,
|
|
|
|
"pieceClass" -> role.toString.toLowerCase
|
|
|
|
)
|
2012-05-14 14:36:32 -06:00
|
|
|
}
|
|
|
|
|
2014-01-31 18:02:32 -07:00
|
|
|
case class PlayerMessage(line: PlayerLine) extends Event {
|
2019-12-13 07:30:20 -07:00
|
|
|
def typ = "message"
|
|
|
|
def data = lila.chat.JsonView(line)
|
2012-05-14 14:36:32 -06:00
|
|
|
override def owner = true
|
2014-01-31 18:02:32 -07:00
|
|
|
override def troll = false
|
2012-05-14 14:36:32 -06:00
|
|
|
}
|
|
|
|
|
2014-01-31 18:02:32 -07:00
|
|
|
case class UserMessage(line: UserLine, w: Boolean) extends Event {
|
2019-12-13 07:30:20 -07:00
|
|
|
def typ = "message"
|
|
|
|
def data = lila.chat.JsonView(line)
|
|
|
|
override def troll = line.troll
|
2014-01-31 18:02:32 -07:00
|
|
|
override def watcher = w
|
2019-12-13 07:30:20 -07:00
|
|
|
override def owner = !w
|
2012-06-10 08:48:06 -06:00
|
|
|
}
|
|
|
|
|
2017-07-07 06:59:54 -06:00
|
|
|
// for mobile app BC only
|
2015-06-16 03:33:49 -06:00
|
|
|
case class End(winner: Option[Color]) extends Event {
|
2019-12-13 07:30:20 -07:00
|
|
|
def typ = "end"
|
2015-08-14 05:14:50 -06:00
|
|
|
def data = Json.toJson(winner)
|
2012-05-14 14:36:32 -06:00
|
|
|
}
|
|
|
|
|
2017-07-07 06:59:54 -06:00
|
|
|
case class EndData(game: Game, ratingDiff: Option[RatingDiffs]) extends Event {
|
|
|
|
def typ = "endData"
|
2019-12-13 07:30:20 -07:00
|
|
|
def data =
|
|
|
|
Json
|
|
|
|
.obj(
|
|
|
|
"winner" -> game.winnerColor,
|
|
|
|
"status" -> game.status
|
2017-07-22 10:14:19 -06:00
|
|
|
)
|
2019-12-13 07:30:20 -07:00
|
|
|
.add("clock" -> game.clock.map { c =>
|
|
|
|
Json.obj(
|
|
|
|
"wc" -> c.remainingTime(Color.White).centis,
|
|
|
|
"bc" -> c.remainingTime(Color.Black).centis
|
|
|
|
)
|
|
|
|
})
|
|
|
|
.add("ratingDiff" -> ratingDiff.map { rds =>
|
|
|
|
Json.obj(
|
|
|
|
Color.White.name -> rds.white,
|
|
|
|
Color.Black.name -> rds.black
|
|
|
|
)
|
|
|
|
})
|
|
|
|
.add("boosted" -> game.boosted)
|
2017-07-07 06:59:54 -06:00
|
|
|
}
|
|
|
|
|
2014-10-03 02:10:12 -06:00
|
|
|
case object Reload extends Empty {
|
|
|
|
def typ = "reload"
|
|
|
|
}
|
|
|
|
case object ReloadOwner extends Empty {
|
2019-12-13 07:30:20 -07:00
|
|
|
def typ = "reload"
|
2013-05-18 09:26:37 -06:00
|
|
|
override def owner = true
|
|
|
|
}
|
2013-05-18 07:51:26 -06:00
|
|
|
|
2017-07-07 04:53:47 -06:00
|
|
|
private def reloadOr[A: Writes](typ: String, data: A) = Json.obj("t" -> typ, "d" -> data)
|
|
|
|
|
|
|
|
// use t:reload for mobile app BC,
|
|
|
|
// but send extra data for the web to avoid reloading
|
2017-07-07 05:13:15 -06:00
|
|
|
case class RematchOffer(by: Option[Color]) extends Event {
|
2019-12-13 07:30:20 -07:00
|
|
|
def typ = "reload"
|
|
|
|
def data = reloadOr("rematchOffer", by)
|
2017-07-07 04:23:02 -06:00
|
|
|
override def owner = true
|
|
|
|
}
|
|
|
|
|
|
|
|
case class RematchTaken(nextId: Game.ID) extends Event {
|
2019-12-13 07:30:20 -07:00
|
|
|
def typ = "reload"
|
2017-07-07 04:53:47 -06:00
|
|
|
def data = reloadOr("rematchTaken", nextId)
|
2017-07-07 04:23:02 -06:00
|
|
|
}
|
|
|
|
|
2017-07-07 05:13:25 -06:00
|
|
|
case class DrawOffer(by: Option[Color]) extends Event {
|
2021-03-11 11:15:32 -07:00
|
|
|
def typ = "reload"
|
|
|
|
def data = reloadOr("drawOffer", by)
|
2017-07-07 05:13:25 -06:00
|
|
|
}
|
|
|
|
|
2017-07-17 08:59:50 -06:00
|
|
|
case class ClockInc(color: Color, time: Centis) extends Event {
|
|
|
|
def typ = "clockInc"
|
2020-05-05 22:11:15 -06:00
|
|
|
def data =
|
|
|
|
Json.obj(
|
|
|
|
"color" -> color,
|
|
|
|
"time" -> time.centis
|
|
|
|
)
|
2017-07-17 08:59:50 -06:00
|
|
|
}
|
|
|
|
|
2017-07-08 06:49:10 -06:00
|
|
|
sealed trait ClockEvent extends Event
|
|
|
|
|
2017-07-22 10:18:07 -06:00
|
|
|
case class Clock(white: Centis, black: Centis, nextLagComp: Option[Centis] = None) extends ClockEvent {
|
2012-05-14 14:36:32 -06:00
|
|
|
def typ = "clock"
|
2019-12-13 07:30:20 -07:00
|
|
|
def data =
|
|
|
|
Json
|
|
|
|
.obj(
|
|
|
|
"white" -> white.toSeconds,
|
|
|
|
"black" -> black.toSeconds
|
|
|
|
)
|
|
|
|
.add("lag" -> nextLagComp.collect { case Centis(c) if c > 1 => c })
|
2012-05-14 14:36:32 -06:00
|
|
|
}
|
|
|
|
object Clock {
|
2020-05-05 22:11:15 -06:00
|
|
|
def apply(clock: ChessClock): Clock =
|
|
|
|
Clock(
|
|
|
|
clock remainingTime Color.White,
|
|
|
|
clock remainingTime Color.Black,
|
|
|
|
clock lagCompEstimate clock.color
|
|
|
|
)
|
2012-05-14 14:36:32 -06:00
|
|
|
}
|
|
|
|
|
2015-08-18 06:21:06 -06:00
|
|
|
case class Berserk(color: Color) extends Event {
|
2019-12-13 07:30:20 -07:00
|
|
|
def typ = "berserk"
|
2015-08-18 06:21:06 -06:00
|
|
|
def data = Json.toJson(color)
|
|
|
|
}
|
|
|
|
|
2017-07-08 06:49:10 -06:00
|
|
|
case class CorrespondenceClock(white: Float, black: Float) extends ClockEvent {
|
2019-12-13 07:30:20 -07:00
|
|
|
def typ = "cclock"
|
2014-11-30 03:22:23 -07:00
|
|
|
def data = Json.obj("white" -> white, "black" -> black)
|
|
|
|
}
|
|
|
|
object CorrespondenceClock {
|
|
|
|
def apply(clock: lila.game.CorrespondenceClock): CorrespondenceClock =
|
|
|
|
CorrespondenceClock(clock.whiteTime, clock.blackTime)
|
|
|
|
}
|
|
|
|
|
2014-07-31 13:06:22 -06:00
|
|
|
case class CheckCount(white: Int, black: Int) extends Event {
|
|
|
|
def typ = "checkCount"
|
2020-05-05 22:11:15 -06:00
|
|
|
def data =
|
|
|
|
Json.obj(
|
|
|
|
"white" -> white,
|
|
|
|
"black" -> black
|
|
|
|
)
|
2014-07-31 13:06:22 -06:00
|
|
|
}
|
|
|
|
|
2015-03-31 07:59:44 -06:00
|
|
|
case class State(
|
|
|
|
color: Color,
|
|
|
|
turns: Int,
|
2015-04-01 04:38:47 -06:00
|
|
|
status: Option[Status],
|
2015-08-14 05:14:50 -06:00
|
|
|
winner: Option[Color],
|
2015-03-31 07:59:44 -06:00
|
|
|
whiteOffersDraw: Boolean,
|
2017-02-14 08:34:07 -07:00
|
|
|
blackOffersDraw: Boolean
|
|
|
|
) extends Event {
|
2012-05-14 14:36:32 -06:00
|
|
|
def typ = "state"
|
2019-12-13 07:30:20 -07:00
|
|
|
def data =
|
|
|
|
Json
|
|
|
|
.obj(
|
|
|
|
"color" -> color,
|
|
|
|
"turns" -> turns
|
|
|
|
)
|
|
|
|
.add("status" -> status)
|
|
|
|
.add("winner" -> winner)
|
|
|
|
.add("wDraw" -> whiteOffersDraw)
|
|
|
|
.add("bDraw" -> blackOffersDraw)
|
2012-05-14 14:36:32 -06:00
|
|
|
}
|
|
|
|
|
2015-04-21 13:21:26 -06:00
|
|
|
case class TakebackOffers(
|
|
|
|
white: Boolean,
|
2017-02-14 08:34:07 -07:00
|
|
|
black: Boolean
|
|
|
|
) extends Event {
|
2015-04-21 13:21:26 -06:00
|
|
|
def typ = "takebackOffers"
|
2019-12-13 07:30:20 -07:00
|
|
|
def data =
|
|
|
|
Json
|
|
|
|
.obj()
|
|
|
|
.add("white" -> white)
|
|
|
|
.add("black" -> black)
|
2015-04-21 13:22:55 -06:00
|
|
|
override def owner = true
|
2015-04-21 13:21:26 -06:00
|
|
|
}
|
|
|
|
|
2012-05-14 14:36:32 -06:00
|
|
|
case class Crowd(
|
|
|
|
white: Boolean,
|
|
|
|
black: Boolean,
|
2018-12-12 05:44:13 -07:00
|
|
|
watchers: Option[JsValue]
|
2017-02-14 08:34:07 -07:00
|
|
|
) extends Event {
|
2012-05-14 14:36:32 -06:00
|
|
|
def typ = "crowd"
|
2019-12-13 07:30:20 -07:00
|
|
|
def data =
|
|
|
|
Json
|
|
|
|
.obj(
|
|
|
|
"white" -> white,
|
|
|
|
"black" -> black
|
|
|
|
)
|
|
|
|
.add("watchers" -> watchers)
|
2012-05-14 14:36:32 -06:00
|
|
|
}
|
|
|
|
}
|