Lobby timeline and messages, ping hook id

pull/1/merge
Thibault Duplessis 2012-03-25 22:43:18 +02:00
parent 9dac236e3d
commit 875516529b
23 changed files with 222 additions and 93 deletions

View File

@ -4,8 +4,6 @@ import lila.system.model._
import play.api.data._
import play.api.data.Forms._
import lila.system.model.Hook
object DataForm {
type MoveData = (String, String, Option[String], Option[Int])
@ -22,20 +20,23 @@ object DataForm {
"message" -> nonEmptyText
))
val entryGameForm = Form(entryGameMapping)
type EntryData = String
val entryForm = Form(single(
"entry" -> nonEmptyText
))
type JoinData = (String, String, EntryGame)
type JoinData = (String, String, EntryData)
val joinForm = Form(tuple(
"redirect" -> nonEmptyText,
"messages" -> nonEmptyText,
"entry" -> entryGameMapping
"entry" -> nonEmptyText
))
type RematchData = (String, String, EntryGame)
type RematchData = (String, String, EntryData)
val rematchForm = Form(tuple(
"whiteRedirect" -> nonEmptyText,
"blackRedirect" -> nonEmptyText,
"entry" -> entryGameMapping
"entry" -> nonEmptyText
))
private type MessagesData = String
@ -48,15 +49,4 @@ object DataForm {
type DrawData = MessagesData
val drawForm = messagesForm
private val entryGameMapping = mapping(
"id" -> nonEmptyText,
"players" -> list(mapping(
"u" -> optional(nonEmptyText),
"ue" -> nonEmptyText
)(EntryPlayer.apply)(EntryPlayer.unapply)),
"variant" -> nonEmptyText,
"rated" -> boolean,
"clock" -> list(number)
)(EntryGame.apply)(EntryGame.unapply)
}

View File

@ -1,6 +1,5 @@
package controllers
import lila.system.model.EntryGame
import lila.http._
import DataForm._
@ -39,8 +38,8 @@ object AppApiC extends LilaController {
ValidIOk[String](endForm)(msgs api.end(gameId, msgs))
}
def start = Action { implicit request =>
ValidIOk[EntryGame](entryGameForm)(entryGame api.start(entryGame))
def start(gameId: String) = Action { implicit request =>
ValidIOk[EntryData](entryForm)(entryData api.start(gameId, entryData))
}
def join(fullId: String) = Action { implicit request

View File

@ -27,10 +27,11 @@ object AppXhrC extends LilaController {
def ping() = Action { implicit request =>
JsonOk(env.pinger.ping(
get("username"),
get("player_key"),
get("watcher"),
get("get_nb_watchers")
username = get("username"),
playerKey = get("player_key"),
watcherKey = get("watcher"),
getNbWatchers = get("get_nb_watchers"),
hookId = get("hook_id")
).unsafePerformIO)
}

View File

@ -1,6 +1,6 @@
package controllers
import lila.system.model.{ Hook, EntryGame }
import lila.system.model.Hook
import lila.http._
import DataForm._
@ -12,7 +12,7 @@ object LobbyApiC extends LilaController {
private val api = env.lobbyApi
def join(gameId: String, color: String) = Action { implicit request
ValidIOk[EntryGame](entryGameForm)(ec api.join(gameId, color, ec))
ValidIOk[EntryData](entryForm)(entry api.join(gameId, color, entry))
}
def create(hookOwnerId: String) = Action {
@ -26,4 +26,8 @@ object LobbyApiC extends LilaController {
def alive(hookOwnerId: String) = Action {
IOk(api.alive(hookOwnerId))
}
def message = Action {
IOk(api.messageRefresh)
}
}

View File

@ -20,6 +20,7 @@ object LobbyXhrC extends LilaController {
hookId,
getIntOr("auth", 0) == 1,
getIntOr("state", 0),
getIntOr("messageId", 0),
getIntOr("entryId", 0)
).unsafePerformIO)
}

View File

@ -25,6 +25,8 @@ sealed trait Clock {
def limitInMinutes = limitInSeconds / 60
def incrementInSeconds = increment / 1000
def estimateTotalTime = limit + 30 * increment
def step: RunningClock

View File

@ -7,6 +7,7 @@ mongo {
user = user
hook = hook
entry = lobby_entry
message = lobby_message
}
}
redis {
@ -22,8 +23,9 @@ lobby {
duration = 10 seconds
#duration = 2 seconds
sleep = 250 milliseconds
max_entries = 12
}
message.max = 30
entry.max = 12
}
memo {
version.timeout = 30 minutes

View File

@ -11,7 +11,7 @@ POST /move/:fullId controllers.AppXhrC.move(fullId: String)
# App Private API
POST /api/update-version/:gameId controllers.AppApiC.updateVersion(gameId: String)
POST /api/start controllers.AppApiC.start
POST /api/start/:gameId controllers.AppApiC.start(gameId: String)
POST /api/end/:gameId controllers.AppApiC.end(gameId: String)
POST /api/talk/:fullId controllers.AppApiC.talk(fullId: String)
POST /api/join/:fullId controllers.AppApiC.join(fullId: String)
@ -34,6 +34,7 @@ GET /api/lobby/preload controllers.LobbyXhrC.syncWithoutHook
POST /api/lobby/create/:hookOwnerId controllers.LobbyApiC.create(hookOwnerId: String)
POST /api/lobby/remove/:hookId controllers.LobbyApiC.remove(hookId: String)
POST /api/lobby/alive/:hookOwnerId controllers.LobbyApiC.alive(hookOwnerId: String)
POST /api/lobby/message controllers.LobbyApiC.message
# Useless, but play2 needs it
GET /assets/*file controllers.Assets.at(path="/public", file)

View File

@ -10,19 +10,19 @@ case class AppApi(
gameRepo: GameRepo,
versionMemo: VersionMemo,
aliveMemo: AliveMemo,
addEntry: EntryGame IO[Unit]) extends IOTools {
addEntry: (DbGame, String) IO[Unit]) extends IOTools {
def join(
fullId: String,
url: String,
messages: String,
entryGame: EntryGame): IO[Unit] = for {
entryData: String): IO[Unit] = for {
gameAndPlayer gameRepo player fullId
(g1, player) = gameAndPlayer
g2 = g1 withEvents decodeMessages(messages)
g3 = g2.withEvents(g2.opponent(player).color, List(RedirectEvent(url)))
_ save(g1, g3)
_ addEntry(entryGame)
_ addEntry(g3, entryData)
} yield ()
def talk(gameId: String, author: String, message: String): IO[Unit] = for {
@ -37,7 +37,10 @@ case class AppApi(
_ save(g1, g2)
} yield ()
def start(entryGame: EntryGame): IO[Unit] = addEntry(entryGame)
def start(gameId: String, entryData: String): IO[Unit] = for {
game gameRepo game gameId
_ addEntry(game, entryData)
} yield ()
def acceptRematch(
gameId: String,
@ -45,7 +48,7 @@ case class AppApi(
colorName: String,
whiteRedirect: String,
blackRedirect: String,
entryGame: EntryGame): IO[Unit] = for {
entryData: String): IO[Unit] = for {
color ioColor(colorName)
g1 gameRepo game gameId
g2 = g1.withEvents(
@ -54,7 +57,7 @@ case class AppApi(
_ save(g1, g2)
_ aliveMemo.put(newGameId, !color)
_ aliveMemo.transfer(gameId, !color, newGameId, color)
_ addEntry(entryGame)
_ addEntry(g2, entryData)
} yield ()
def updateVersion(gameId: String): IO[Unit] =

View File

@ -42,7 +42,7 @@ final class AppSyncer(
) filterValues (null !=)
} getOrElse failMap
}
} except (e {println(e.getMessage);io(failMap)})
} except (e io(failMap))
private def renderEvents(events: List[Event], isPrivate: Boolean) =
if (isPrivate) events map {

View File

@ -10,6 +10,7 @@ case class LobbyApi(
gameRepo: GameRepo,
entryRepo: EntryRepo,
lobbyMemo: LobbyMemo,
messageMemo: MessageMemo,
entryMemo: EntryMemo,
versionMemo: VersionMemo,
aliveMemo: AliveMemo,
@ -18,13 +19,13 @@ case class LobbyApi(
def join(
gameId: String,
colorName: String,
entryGame: EntryGame): IO[Unit] = for {
entryData: String): IO[Unit] = for {
color ioColor(colorName)
g1 gameRepo game gameId
game gameRepo game gameId
_ aliveMemo.put(gameId, color)
_ aliveMemo.put(gameId, !color)
_ versionInc
_ addEntry(entryGame)
_ addEntry(game, entryData)
} yield ()
def create(hookOwnerId: String): IO[Unit] = for {
@ -39,10 +40,13 @@ case class LobbyApi(
def alive(hookOwnerId: String): IO[Unit] = hookMemo put hookOwnerId
def messageRefresh: IO[Unit] = messageMemo.refresh
private[system] def versionInc: IO[Int] = lobbyMemo++
private[system] def addEntry(entryGame: EntryGame): IO[Unit] = for {
nextId (entryMemo++)
_ io { entryRepo insert Entry(nextId, entryGame) }
} yield ()
private[system] def addEntry(game: DbGame, data: String): IO[Unit] =
Entry.build(game, data).fold(
f (entryMemo++) map (id entryRepo insert f(id)),
io()
)
}

View File

@ -7,17 +7,19 @@ import scalaz.effects._
import scalaz.NonEmptyList
import scala.annotation.tailrec
import scala.math.max
import org.apache.commons.lang3.StringEscapeUtils.escapeXml
final class LobbySyncer(
hookRepo: HookRepo,
gameRepo: GameRepo,
messageRepo: MessageRepo,
entryRepo: EntryRepo,
lobbyMemo: LobbyMemo,
hookMemo: HookMemo,
messageMemo: MessageMemo,
entryMemo: EntryMemo,
duration: Int,
sleep: Int,
maxEntries: Int) {
sleep: Int) {
type Response = Map[String, Any]
@ -25,12 +27,13 @@ final class LobbySyncer(
myHookId: Option[String],
auth: Boolean,
version: Int,
messageId: Int,
entryId: Int): IO[Response] = for {
_ myHookId.fold(hookMemo.put, io())
newVersion wait(version, entryId)
newVersion wait(version, messageId, entryId)
hooks if (auth) hookRepo.allOpen else hookRepo.allOpenCasual
res {
val response = () stdResponse(newVersion, hooks, myHookId, entryId)
val response = () stdResponse(newVersion, hooks, myHookId, messageId, entryId)
myHookId some { hookResponse(_, response) } none response()
}
} yield res
@ -39,7 +42,7 @@ final class LobbySyncer(
hookRepo ownedHook myHookId flatMap { hookOption
hookOption.fold(
hook hook.game.fold(
ref gameRepo game ref.getId.toString.pp map { game
ref gameRepo game ref.getId.toString map { game
Map("redirect" -> (game fullIdOf game.creatorColor))
},
response()
@ -52,27 +55,43 @@ final class LobbySyncer(
version: Int,
hooks: List[Hook],
myHookId: Option[String],
messageId: Int,
entryId: Int): IO[Response] = for {
entries
if (entryId == 0) entryRepo recent maxEntries
else entryRepo since max(entryMemo.id - maxEntries, entryId)
messages if (messageId == 0) messageRepo.recent
else messageRepo since max(messageMemo.id - messageRepo.max, messageId)
entries if (entryId == 0) entryRepo.recent
else entryRepo since max(entryMemo.id - entryRepo.max, entryId)
} yield Map(
"state" -> version,
"pool" -> {
if (hooks.nonEmpty) Map("hooks" -> renderHooks(hooks, myHookId).toMap)
else Map("message" -> "No game available right now, create one!")
},
"chat" -> null,
"chat" -> messages.toNel.fold(
renderMessages,
Map("id" -> messageId, "messages" -> Nil)
),
"timeline" -> entries.toNel.fold(
renderTimeline,
renderEntries,
Map("id" -> entryId, "entries" -> Nil)
)
)
private def renderTimeline(entries: NonEmptyList[Entry]) = Map(
private def renderMessages(messages: NonEmptyList[Message]) = Map(
"id" -> messages.head.id,
"messages" -> (messages.list.reverse map { message
Map(
"id" -> message.id,
"u" -> message.username,
"m" -> escapeXml(message.message)
)
})
)
private def renderEntries(entries: NonEmptyList[Entry]) = Map(
"id" -> entries.head.id,
"entries" -> (entries.list.reverse map { entry =>
"<td>%s</td><td>%s</td><td class='trans_me'>%s</td><td>%s</td><td class='trans_me'>%s</td>".format(
"entries" -> (entries.list.reverse map { entry
"<td>%s</td><td>%s</td><td class='trans_me'>%s</td><td class='trans_me'>%s</td><td class='trans_me'>%s</td>".format(
"<a class='watch' href='/%s'></a>" format entry.data.id,
entry.data.players map { p
p.u.fold(
@ -95,11 +114,12 @@ final class LobbySyncer(
}
}
private def wait(version: Int, entryId: Int): IO[Int] = io {
private def wait(version: Int, messageId: Int, entryId: Int): IO[Int] = io {
@tailrec
def wait(loop: Int): Int = {
if (loop == 0 ||
lobbyMemo.version != version ||
messageMemo.id != messageId ||
entryMemo.id != entryId) lobbyMemo.version
else { Thread sleep sleep; wait(loop - 1) }
}

View File

@ -7,16 +7,19 @@ import scalaz.effects._
final class Pinger(
aliveMemo: AliveMemo,
usernameMemo: UsernameMemo,
watcherMemo: WatcherMemo) {
watcherMemo: WatcherMemo,
hookMemo: HookMemo) {
def ping(
username: Option[String],
playerKey: Option[String],
watcherKey: Option[String],
getNbWatchers: Option[String]): IO[Map[String, Any]] = for {
getNbWatchers: Option[String],
hookId: Option[String]): IO[Map[String, Any]] = for {
_ optionIO(playerKey, aliveMemo.put)
_ optionIO(username, usernameMemo.put)
_ optionIO(watcherKey, watcherMemo.put)
_ optionIO(hookId, hookMemo.put)
} yield flatten(Map(
"nbp" -> Some(aliveMemo.count),
"nbw" -> (getNbWatchers map watcherMemo.count)

View File

@ -39,6 +39,7 @@ final class SystemEnv(config: Config) {
entryRepo = entryRepo,
versionMemo = versionMemo,
lobbyMemo = lobbyMemo,
messageMemo = messageMemo,
entryMemo = entryMemo,
aliveMemo = aliveMemo,
hookMemo = hookMemo)
@ -46,18 +47,20 @@ final class SystemEnv(config: Config) {
lazy val lobbySyncer = new LobbySyncer(
hookRepo = hookRepo,
gameRepo = gameRepo,
messageRepo = messageRepo,
entryRepo = entryRepo,
lobbyMemo = lobbyMemo,
hookMemo = hookMemo,
messageMemo = messageMemo,
entryMemo = entryMemo,
duration = getMilliseconds("lobby.sync.duration"),
sleep = getMilliseconds("lobby.sync.sleep"),
maxEntries = config getInt "lobby.sync.max_entries")
sleep = getMilliseconds("lobby.sync.sleep"))
lazy val pinger = new Pinger(
aliveMemo = aliveMemo,
usernameMemo = usernameMemo,
watcherMemo = watcherMemo)
watcherMemo = watcherMemo,
hookMemo = hookMemo)
lazy val ai: Ai = craftyAi
@ -75,7 +78,12 @@ final class SystemEnv(config: Config) {
mongodb(config getString "mongo.collection.hook"))
lazy val entryRepo = new EntryRepo(
mongodb(config getString "mongo.collection.entry"))
collection = mongodb(config getString "mongo.collection.entry"),
max = config getInt "lobby.entry.max")
lazy val messageRepo = new MessageRepo(
collection = mongodb(config getString "mongo.collection.message"),
max = config getInt "lobby.message.max")
lazy val mongodb = MongoConnection(
config getString "mongo.host",
@ -104,6 +112,9 @@ final class SystemEnv(config: Config) {
lazy val entryMemo = new EntryMemo(
getId = entryRepo.lastId)
lazy val messageMemo = new MessageMemo(
getId = messageRepo.lastId)
def getMilliseconds(name: String): Int = (config getMilliseconds name).toInt
}

View File

@ -3,31 +3,7 @@ package db
import model.Entry
import com.novus.salat._
import com.novus.salat.dao._
import com.mongodb.casbah.MongoCollection
import com.mongodb.casbah.Imports._
import scalaz.effects._
class EntryRepo(collection: MongoCollection)
extends SalatDAO[Entry, String](collection) {
private val idSelector = DBObject("_id" -> true)
private val idSorter = DBObject("_id" -> -1)
val lastId: () IO[Option[Int]] = () io {
collection.find(DBObject(), idSelector)
.sort(idSorter)
.limit(1)
.next()
.getAs[Int]("_id")
}
def recent(max: Int) = io {
find(DBObject()).sort(idSorter).limit(max).toList
}
def since(id: Int): IO[List[Entry]] = io {
find("_id" $gt id).sort(idSorter).toList
}
}
class EntryRepo(collection: MongoCollection, val max: Int)
extends TimelineRepo[Entry](collection, max)

View File

@ -70,7 +70,7 @@ class GameRepo(collection: MongoCollection)
d("clock.timer", _.clock.get.timer)
}
MongoDBObject("$set" -> builder.result.pp)
MongoDBObject("$set" -> builder.result)
}
def insert(game: DbGame): IO[Option[String]] = io {

View File

@ -0,0 +1,9 @@
package lila.system
package db
import model.Message
import com.mongodb.casbah.MongoCollection
class MessageRepo(collection: MongoCollection, val max: Int)
extends TimelineRepo[Message](collection, max)

View File

@ -0,0 +1,32 @@
package lila.system
package db
import model.Message
import com.novus.salat._
import com.novus.salat.dao._
import com.mongodb.casbah.MongoCollection
import com.mongodb.casbah.Imports._
import scalaz.effects._
abstract class TimelineRepo[A <: AnyRef](collection: MongoCollection, max: Int)(implicit m: Manifest[A]) extends SalatDAO[A, Int](collection) {
val idSelector = DBObject("_id" -> true)
val idSorter = DBObject("_id" -> -1)
val lastId: () IO[Option[Int]] = () io {
collection.find(DBObject(), idSelector)
.sort(idSorter)
.limit(1)
.next()
.getAs[Int]("_id")
}
val recent = io {
find(DBObject()).sort(idSorter).limit(max).toList
}
def since(id: Int): IO[List[A]] = io {
find("_id" $gt id).sort(idSorter).toList
}
}

View File

@ -5,7 +5,7 @@ import com.mongodb.casbah.MongoCollection
import com.mongodb.casbah.Imports._
import scalaz.effects._
final class EntryMemo(getId: () => IO[Option[Int]]) {
final class EntryMemo(getId: () IO[Option[Int]]) {
private var privateId: Int = _

View File

@ -0,0 +1,26 @@
package lila.system
package memo
import com.mongodb.casbah.MongoCollection
import com.mongodb.casbah.Imports._
import scalaz.effects._
final class MessageMemo(getId: () IO[Option[Int]]) {
private var privateId: Int = _
refresh.unsafePerformIO
def refresh = for {
idOption getId()
} yield {
privateId = idOption err "No last message found"
}
def ++ : IO[Int] = io {
privateId = privateId + 1
privateId
}
def id: Int = privateId
}

View File

@ -15,4 +15,36 @@ case class EntryGame(
players: List[EntryPlayer],
variant: String,
rated: Boolean,
clock: List[Int] = Nil)
clock: Option[List[Int]])
object Entry {
def build(game: DbGame, encodedData: String): Option[Int Entry] =
encodedData.split('$').toList match {
case wu :: wue :: bu :: bue :: Nil Some(
(id: Int)
new Entry(
id = id,
EntryGame(
id = game.id,
players = List(
EntryPlayer(
u = wu.some filterNot (_.isEmpty),
ue = wue
),
EntryPlayer(
u = bu.some filterNot (_.isEmpty),
ue = bue
)
),
variant = game.variant.name,
rated = game.isRated,
clock = game.clock map { c
List(c.limitInMinutes, c.incrementInSeconds)
}
)
)
)
case _ None
}
}

View File

@ -0,0 +1,10 @@
package lila.system
package model
import com.novus.salat.annotations._
case class Message(
@Key("_id") id: Int,
username: String,
message: String) {
}

View File

@ -1,7 +1,10 @@
package lila.system
package model
sealed abstract class Variant(val id: Int)
sealed abstract class Variant(val id: Int) {
lazy val name = toString.toLowerCase
}
case object Standard extends Variant(1)
case object Chess960 extends Variant(2)