Remove all mongodb objectids. They suck.

This commit is contained in:
Thibault Duplessis 2012-05-28 15:23:46 +02:00
parent d0c41f3a33
commit 2adb874471
43 changed files with 160 additions and 218 deletions

View file

@ -22,7 +22,7 @@ object User extends LilaController {
def show(username: String) = showFilter(username, "all", 1)
def showFilter(username: String, filterName: String, page: Int) = Open { implicit ctx
IOptionIOk(userRepo byUsername username) { u
IOptionIOk(userRepo byId username) { u
u.enabled.fold(
env.user.userInfo(u, ctx) map { info
val filters = user.GameFilterMenu(info, ctx.me, filterName)
@ -79,7 +79,7 @@ object User extends LilaController {
_
IORedirect {
for {
uOption userRepo byUsername username
uOption userRepo byId username
_ uOption.filter(_.elo > UserModel.STARTING_ELO).fold(
u eloUpdater.adjust(u, UserModel.STARTING_ELO) flatMap { _
userRepo setEngine u
@ -101,5 +101,5 @@ object User extends LilaController {
def export(username: String) = TODO
private val onlineUsers: IO[List[UserModel]] =
userRepo byUsernames env.user.usernameMemo.keys
userRepo byIds env.user.usernameMemo.keys
}

View file

@ -2,7 +2,7 @@ package lila
package core
import com.mongodb.casbah.MongoConnection
import com.mongodb.{ DBRef, Mongo, MongoOptions, ServerAddress MongoServer }
import com.mongodb.{ Mongo, MongoOptions, ServerAddress MongoServer }
import akka.actor._
@ -26,8 +26,7 @@ final class CoreEnv private (application: Application, val settings: Settings) {
lazy val user = new lila.user.UserEnv(
settings = settings,
mongodb = mongodb.apply _,
gameRepo = game.gameRepo,
dbRef = namespace id mongodb.ref(namespace, id))
gameRepo = game.gameRepo)
lazy val forum = new lila.forum.ForumEnv(
settings = settings,
@ -57,14 +56,13 @@ final class CoreEnv private (application: Application, val settings: Settings) {
userRepo = user.userRepo,
timelinePush = timeline.push.apply,
roundMessenger = round.messenger,
ai = ai.ai,
userDbRef = user.userRepo.dbRef)
ai = ai.ai)
lazy val timeline = new lila.timeline.TimelineEnv(
settings = settings,
mongodb = mongodb.apply _,
lobbyNotify = lobby.socket.addEntry,
getUsername = user.cached.username)
getUsername = user.cached.usernameOrAnonymous)
lazy val ai = new lila.ai.AiEnv(
settings = settings)

View file

@ -3,7 +3,6 @@ package forum
import com.novus.salat._
import com.novus.salat.dao._
import com.mongodb.DBRef
import com.mongodb.casbah.MongoCollection
import com.mongodb.casbah.Imports._
import scalaz.effects._

View file

@ -8,10 +8,10 @@ import play.api.templates.Html
trait ForumHelper { self: UserHelper
def authorName(post: Post) =
post.userIdString.fold(userIdToUsername, post.showAuthor)
post.userId.fold(userIdToUsername, post.showAuthor)
def authorLink(post: Post, cssClass: Option[String] = None) =
post.userIdString.fold(
post.userId.fold(
userId userIdLink(userId.some, cssClass),
Html("""<span class="%s">%s</span>"""
.format(cssClass | "", authorName(post)))

View file

@ -3,7 +3,6 @@ package forum
import org.joda.time.DateTime
import com.novus.salat.annotations.Key
import com.mongodb.casbah.Imports.ObjectId
import ornicar.scalalib.OrnicarRandom
import user.User
@ -12,14 +11,12 @@ case class Post(
@Key("_id") id: String,
topicId: String,
author: Option[String],
userId: Option[ObjectId],
userId: Option[String],
text: String,
number: Int,
createdAt: DateTime) {
def showAuthor = (author map (_.trim) filter ("" !=)) | User.anonymous
def userIdString: Option[String] = userId map (_.toString)
}
object Post {
@ -29,7 +26,7 @@ object Post {
def apply(
topicId: String,
author: Option[String],
userId: Option[ObjectId],
userId: Option[String],
text: String,
number: Int): Post = Post(
id = OrnicarRandom nextAsciiString idSize,

View file

@ -3,7 +3,6 @@ package forum
import com.novus.salat._
import com.novus.salat.dao._
import com.mongodb.DBRef
import com.mongodb.casbah.MongoCollection
import com.mongodb.casbah.Imports._
import scalaz.effects._

View file

@ -4,8 +4,6 @@ package game
import chess._
import user.User
import com.mongodb.DBRef
case class DbPlayer(
id: String,
color: Color,
@ -16,7 +14,7 @@ case class DbPlayer(
isOfferingRematch: Boolean = false,
lastDrawOffer: Option[Int] = None,
isProposingTakeback: Boolean = false,
user: Option[DBRef] = None,
userId: Option[String] = None,
elo: Option[Int] = None,
eloDiff: Option[Int] = None,
moveTimes: String = "",
@ -34,23 +32,17 @@ case class DbPlayer(
ps = encodePieces(allPieces)
)
def withUser(user: User)(dbRef: User DBRef): DbPlayer = copy(
user = dbRef(user).some,
def withUser(user: User): DbPlayer = copy(
userId = user.id.some,
elo = user.elo.some)
def isAi = aiLevel.isDefined
def isHuman = !isAi
def userId: Option[String] = user map (_.getId.toString)
def hasUser = userId.isDefined
def hasUser = user.isDefined
def isUser(u: User) = user.fold(_.getId == u.id, false)
def withUser(u: User, ref: DBRef) = copy(
elo = u.elo.some,
user = ref.some)
def isUser(u: User) = userId.fold(_ == u.id, false)
def wins = isWinner getOrElse false
@ -82,12 +74,12 @@ case class DbPlayer(
aiLevel = aiLevel,
w = isWinner,
elo = elo,
eloDiff = eloDiff,
ed = eloDiff,
isOfferingDraw = if (isOfferingDraw) Some(true) else None,
isOfferingRematch = if (isOfferingRematch) Some(true) else None,
lastDrawOffer = lastDrawOffer,
isProposingTakeback = if (isProposingTakeback) Some(true) else None,
user = user,
userId = userId,
mts = Some(moveTimes) filter ("" !=),
blurs = Some(blurs) filter (0 !=)
)
@ -114,12 +106,12 @@ case class RawDbPlayer(
aiLevel: Option[Int],
w: Option[Boolean],
elo: Option[Int],
eloDiff: Option[Int],
ed: Option[Int],
isOfferingDraw: Option[Boolean],
isOfferingRematch: Option[Boolean],
lastDrawOffer: Option[Int],
isProposingTakeback: Option[Boolean],
user: Option[DBRef],
userId: Option[String],
mts: Option[String],
blurs: Option[Int]) {
@ -132,12 +124,12 @@ case class RawDbPlayer(
aiLevel = aiLevel,
isWinner = w,
elo = elo,
eloDiff = eloDiff,
eloDiff = ed,
isOfferingDraw = isOfferingDraw | false,
isOfferingRematch = isOfferingRematch | false,
lastDrawOffer = lastDrawOffer,
isProposingTakeback = isProposingTakeback | false,
user = user,
userId = userId,
moveTimes = mts | "",
blurs = blurs | 0
)

View file

@ -71,16 +71,16 @@ class GameRepo(collection: MongoCollection)
update(idSelector(id), $set("players.0.eloDiff" -> white, "players.1.eloDiff" -> black))
}
def setUser(id: String, color: Color, dbRef: DBRef, elo: Int) = io {
def setUser(id: String, color: Color, user: User) = io {
val pn = "players.%d".format(color.fold(0, 1))
update(idSelector(id), $set(pn + ".user" -> dbRef, pn + ".elo" -> elo))
update(idSelector(id), $set(pn + ".userId" -> user.id, pn + ".elo" -> user.elo))
}
def finish(id: String, winnerId: Option[String]) = io {
update(
idSelector(id),
winnerId.fold(userId
$set("positionHashes" -> "", "winnerUserId" -> userId),
$set("positionHashes" -> "", "winId" -> userId),
$set("positionHashes" -> ""))
++ $unset(
"players.0.previousMoveTs",
@ -157,22 +157,6 @@ class GameRepo(collection: MongoCollection)
find("_id" $in ids).toList.map(_.decode).flatten sortBy (_.id)
}
def ensureIndexes: IO[Unit] = io {
collection.underlying |> { coll
coll.ensureIndex(DBObject("status" -> 1))
coll.ensureIndex(DBObject("userIds" -> 1))
coll.ensureIndex(DBObject("winnerUserId" -> 1))
coll.ensureIndex(DBObject("turns" -> 1))
coll.ensureIndex(DBObject("updatedAt" -> -1))
coll.ensureIndex(DBObject("createdAt" -> -1))
coll.ensureIndex(DBObject("createdAt" -> -1, "userIds" -> 1))
}
}
def dropIndexes: IO[Unit] = io {
collection.dropIndexes()
}
private def idSelector(game: DbGame): DBObject = idSelector(game.id)
private def idSelector(id: String): DBObject = DBObject("_id" -> id)
}

View file

@ -29,19 +29,19 @@ object Query {
def clock(c: Boolean): DBObject = "clock.l" $exists c
def user(u: User): DBObject = DBObject("userIds" -> u.idString)
def user(u: User): DBObject = DBObject("userIds" -> u.id)
def started(u: User): DBObject = user(u) ++ started
def rated(u: User): DBObject = user(u) ++ rated
def win(u: User): DBObject = DBObject("winnerUserId" -> u.idString)
def win(u: User): DBObject = DBObject("winId" -> u.id)
def draw(u: User): DBObject = user(u) ++ draw
def loss(u: User): DBObject = user(u) ++ finished ++ ("winnerUserId" $ne u.idString)
def loss(u: User): DBObject = user(u) ++ finished ++ ("winId" $ne u.id)
def playing(u: User): DBObject = user(u) ++ playing
def opponents(u1: User, u2: User) = "userIds" $all List(u1.idString, u2.idString)
def opponents(u1: User, u2: User) = "userIds" $all List(u1.id, u2.id)
}

View file

@ -72,7 +72,7 @@ object Hook {
increment = clock map (_.increment),
mode = mode.id,
color = color,
userId = user map (_.idString),
userId = user map (_.id),
username = user.fold(_.username, User.anonymous),
elo = user map (_.elo),
eloRange = eloRange.toString,

View file

@ -8,7 +8,6 @@ import play.api.i18n.Lang
import play.api.i18n.MessagesPlugin
import scalaz.effects._
import com.mongodb.casbah.MongoCollection
import com.mongodb.DBRef
import user.{ User, UserRepo }
import game.{ GameRepo, DbGame }

View file

@ -13,7 +13,7 @@ final class Messenger(
private val urlRegex = """lichess\.org/([\w-]{8})[\w-]{4}""".r
def apply(text: String, username: String): IO[Valid[Message]] = for {
userOption userRepo byUsername username
userOption userRepo byId username
message = for {
user userOption toValid "Unknown user"
msg createMessage(text, user)
@ -35,7 +35,7 @@ final class Messenger(
}
def ban(username: String): IO[Unit] = for {
userOption userRepo byUsername username
userOption userRepo byId username
_ userOption.fold(
user for {
_ userRepo toggleChatBan user

View file

@ -26,7 +26,7 @@ final class DataForm(userRepo: UserRepo) {
))
private def fetchUser(username: String) =
(userRepo byUsername username).unsafePerformIO
(userRepo byId username).unsafePerformIO
private def usernameExists(username: String) =
fetchUser(username).isDefined

View file

@ -2,7 +2,6 @@ package lila
package message
import org.joda.time.DateTime
import com.mongodb.casbah.Imports.ObjectId
import ornicar.scalalib.OrnicarRandom
case class Post(

View file

@ -5,7 +5,6 @@ import user.User
import org.joda.time.DateTime
import com.novus.salat.annotations.Key
import com.mongodb.casbah.Imports.ObjectId
import ornicar.scalalib.OrnicarRandom
case class Thread(
@ -14,9 +13,9 @@ case class Thread(
createdAt: DateTime,
updatedAt: DateTime,
posts: List[Post],
creatorId: ObjectId,
invitedId: ObjectId,
visibleByUserIds: List[ObjectId]) {
creatorId: String,
invitedId: String,
visibleByUserIds: List[String]) {
def +(post: Post) = copy(
posts = posts :+ post,

View file

@ -26,19 +26,19 @@ final class ThreadRepo(
def userNbUnread(user: User): IO[Int] = userNbUnread(user.id)
def userNbUnread(userId: ObjectId): IO[Int] = io {
def userNbUnread(userId: String): IO[Int] = io {
val result = collection.mapReduce(
mapFunction = """function() {
var thread = this, nb = 0;
thread.posts.forEach(function(p) {
if (!p.isRead) {
if (thread.creatorId.equals(ObjectId("%s"))) {
if (thread.creatorId == "%s") {
if (!p.isByCreator) nb++;
} else if (p.isByCreator) nb++;
}
});
if (nb > 0) emit("n", nb);
}""" format userId.toString,
}""" format userId,
reduceFunction = """function(key, values) {
var sum = 0;
for(var i in values) { sum += values[i]; }
@ -81,7 +81,7 @@ final class ThreadRepo(
def visibleByUserQuery(user: User): DBObject =
visibleByUserQuery(user.id)
def visibleByUserQuery(userId: ObjectId): DBObject =
def visibleByUserQuery(userId: String): DBObject =
DBObject("visibleByUserIds" -> userId)
def selectId(id: String) = DBObject("_id" -> id)

View file

@ -12,12 +12,12 @@ final class UnreadCache(threadRepo: ThreadRepo) {
def get(user: User): Int = get(user.id)
def get(userId: ObjectId): Int =
def get(username: String): Int =
cache.getOrElseUpdate(username.toLowerCase, {
(threadRepo userNbUnread user).unsafePerformIO
(threadRepo userNbUnread username).unsafePerformIO
})
def invalidate(user: User) {
cache -= user.usernameCanonical
cache -= user.id
}
}

View file

@ -2,8 +2,7 @@ package lila
package mongodb
import com.mongodb.casbah.MongoConnection
import com.mongodb.casbah.Imports.ObjectId
import com.mongodb.{ DBRef, Mongo, MongoOptions, ServerAddress MongoServer }
import com.mongodb.{ Mongo, MongoOptions, ServerAddress MongoServer }
import core.Settings
@ -14,9 +13,6 @@ final class MongoDbEnv(
def apply(coll: String) = connection(coll)
def ref(namespace: String, id: ObjectId): DBRef =
new DBRef(connection.underlying, namespace, id)
lazy val cache = new Cache(connection(MongoCollectionCache))
lazy val connection = MongoConnection(server, options)(MongoDbName)

View file

@ -85,6 +85,6 @@ trait AuthImpl {
def restoreUser(req: RequestHeader): Option[User] = for {
sessionId req.session.get("sessionId")
username env.securityStore.getUsername(sessionId)
user (env.user.userRepo byUsername username).unsafePerformIO
user (env.user.userRepo byId username).unsafePerformIO
} yield user
}

View file

@ -7,22 +7,20 @@ import user.User
import round.{ Event, Progress, Messenger }
import controllers.routes
import com.mongodb.DBRef
import scalaz.effects._
final class FriendJoiner(
gameRepo: GameRepo,
messenger: Messenger,
timelinePush: DbGame IO[Unit],
userDbRef: User DBRef) {
timelinePush: DbGame IO[Unit]) {
def apply(game: DbGame, user: Option[User]): Valid[IO[(Pov, List[Event])]] =
game.notStarted option {
val color = game.invitedColor
for {
p1 user.fold(
u gameRepo.setUser(game.id, color, userDbRef(u), u.elo) map { _
Progress(game, game.updatePlayer(color, _.withUser(u, userDbRef(u))))
u gameRepo.setUser(game.id, color, u) map { _
Progress(game, game.updatePlayer(color, _ withUser u))
},
io(Progress(game)))
p2 = p1 withGame game.start

View file

@ -8,14 +8,12 @@ import game.{ GameRepo, DbGame, DbPlayer, Pov }
import round.{ Messenger, Progress }
import scalaz.effects._
import com.mongodb.DBRef
final class HookJoiner(
hookRepo: HookRepo,
fisherman: Fisherman,
gameRepo: GameRepo,
userRepo: UserRepo,
userDbRef: User DBRef,
timelinePush: DbGame IO[Unit],
messenger: Messenger) {
@ -51,7 +49,7 @@ final class HookJoiner(
def blame(color: DbGame ChessColor, userOption: Option[User], game: DbGame) =
userOption.fold(
user game.updatePlayer(color(game), _.withUser(user, userDbRef(user))),
user game.updatePlayer(color(game), _ withUser user),
game)
def makeGame(hook: Hook) = DbGame(

View file

@ -8,7 +8,6 @@ import chess.{ Game, Board }
import ai.Ai
import lobby.{ Hook, Fisherman }
import com.mongodb.DBRef
import scalaz.effects._
final class Processor(
@ -17,8 +16,7 @@ final class Processor(
gameRepo: GameRepo,
fisherman: Fisherman,
timelinePush: DbGame IO[Unit],
ai: () Ai,
userDbRef: User DBRef) {
ai: () Ai) {
def ai(config: AiConfig)(implicit ctx: Context): IO[Pov] = for {
_ ctx.me.fold(
@ -27,7 +25,7 @@ final class Processor(
)
pov = config.pov
game = ctx.me.fold(
user pov.game.updatePlayer(pov.color, _.withUser(user, userDbRef(user))),
user pov.game.updatePlayer(pov.color, _ withUser user),
pov.game)
_ gameRepo insert game
_ gameRepo denormalizeStarted game
@ -50,7 +48,7 @@ final class Processor(
)
pov = config.pov
game = ctx.me.fold(
user pov.game.updatePlayer(pov.color, _.withUser(user, userDbRef(user))),
user pov.game.updatePlayer(pov.color, _ withUser user),
pov.game)
_ gameRepo insert game
_ timelinePush(game)

View file

@ -89,7 +89,7 @@ final class Rematcher(
game.player(color).userId.fold(
userId userRepo byId userId map { userOption
userOption.fold(
user player.withUser(user)(userRepo.dbRef),
user player withUser user,
player)
},
io(player))

View file

@ -10,7 +10,6 @@ import user.{ User, UserRepo }
import com.mongodb.casbah.MongoCollection
import scalaz.effects._
import com.mongodb.DBRef
final class SetupEnv(
settings: Settings,
@ -21,8 +20,7 @@ final class SetupEnv(
userRepo: UserRepo,
timelinePush: DbGame IO[Unit],
roundMessenger: Messenger,
ai: () Ai,
userDbRef: User DBRef) {
ai: () Ai) {
import settings._
@ -37,8 +35,7 @@ final class SetupEnv(
gameRepo = gameRepo,
fisherman = fisherman,
timelinePush = timelinePush,
ai = ai,
userDbRef = userDbRef)
ai = ai)
lazy val friendConfigMemo = new FriendConfigMemo(
ttl = SetupFriendConfigMemoTtl)
@ -52,15 +49,13 @@ final class SetupEnv(
lazy val friendJoiner = new FriendJoiner(
gameRepo = gameRepo,
messenger = roundMessenger,
timelinePush = timelinePush,
userDbRef = userDbRef)
timelinePush = timelinePush)
lazy val hookJoiner = new HookJoiner(
hookRepo = hookRepo,
fisherman = fisherman,
gameRepo = gameRepo,
userRepo = userRepo,
userDbRef = userDbRef,
timelinePush = timelinePush,
messenger = roundMessenger)
}

View file

@ -27,7 +27,7 @@ case class UserConfig(
object UserConfig {
def default(user: User) = UserConfig(
id = user.usernameCanonical,
id = user.id,
ai = AiConfig.default,
friend = FriendConfig.default,
hook = HookConfig.default)

View file

@ -19,7 +19,7 @@ class UserConfigRepo(collection: MongoCollection)
} yield ()
def config(user: User): IO[UserConfig] = io {
findOneByID(user.usernameCanonical) flatMap (_.decode)
findOneByID(user.id) flatMap (_.decode)
} except { e
putStrLn("Can't load config: " + e.getMessage) map (_ none[UserConfig])
} map (_ | UserConfig.default(user))

View file

@ -48,7 +48,7 @@ abstract class HubActor[M <: SocketMember](uidTimeout: Int) extends Actor {
setAlive(uid)
member(uid) foreach { m
m.channel push m.username.fold(
u pong ++ JsObject(Seq("m" -> env.message.unreadCache.get(u))),
u pong ++ JsObject(Seq("m" -> JsNumber(unreadMessages(u)))),
pong)
}
}
@ -83,4 +83,7 @@ abstract class HubActor[M <: SocketMember](uidTimeout: Int) extends Actor {
def member(uid: String): Option[M] = members get uid
def usernames: Iterable[String] = members.values.map(_.username).flatten
private def unreadMessages(username: String): Int =
env.message.unreadCache get username
}

View file

@ -2,25 +2,17 @@ package lila
package user
import scala.collection.mutable
import com.mongodb.casbah.Imports.ObjectId
final class Cached(userRepo: UserRepo) {
// idString => username|Anonymous
val usernameCache = mutable.Map[String, String]()
// username => Option[ObjectId]
val idCache = mutable.Map[String, Option[ObjectId]]()
// id => username
val usernameCache = mutable.Map[String, Option[String]]()
def username(userId: String) =
usernameCache.getOrElseUpdate(
userId,
(userRepo username userId).unsafePerformIO | "Anonymous"
userId.toLowerCase,
(userRepo username userId).unsafePerformIO
)
def id(username: String): Option[ObjectId] =
idCache.getOrElseUpdate(
username.toLowerCase,
(userRepo id username).unsafePerformIO
)
def usernameOrAnonymous(userId: String) = username(userId) | User.anonymous
}

View file

@ -12,12 +12,12 @@ final class EloUpdater(
def game(user: User, elo: Int, gameId: String): IO[Unit] = {
val newElo = max(elo, floor)
userRepo.setElo(user.id, newElo) flatMap { _
historyRepo.addEntry(user.usernameCanonical, newElo, Some(gameId))
historyRepo.addEntry(user.id, newElo, Some(gameId))
}
}
def adjust(user: User, elo: Int): IO[Unit] =
userRepo.setElo(user.id, elo) flatMap { _
historyRepo.addEntry(user.usernameCanonical, elo, entryType = HistoryRepo.TYPE_ADJUST)
historyRepo.addEntry(user.id, elo, entryType = HistoryRepo.TYPE_ADJUST)
}
}

View file

@ -2,11 +2,10 @@ package lila
package user
import com.novus.salat.annotations.Key
import com.mongodb.casbah.Imports.ObjectId
import org.joda.time.DateTime
case class User(
@Key("_id") id: ObjectId,
id: String,
username: String,
elo: Int,
nbGames: Int,
@ -18,8 +17,6 @@ case class User(
bio: Option[String] = None,
engine: Boolean = false) {
def usernameCanonical = username.toLowerCase
def disabled = !enabled
def usernameWithElo = "%s (%d)".format(username, elo)
@ -29,8 +26,6 @@ case class User(
def nonEmptyBio = bio filter ("" !=)
def hasGames = nbGames > 0
def idString = id.toString
}
object User {
@ -41,7 +36,7 @@ object User {
// the password is hashed
def apply(username: String): User = User(
id = new ObjectId,
id = username.toLowerCase,
username = username,
elo = STARTING_ELO,
nbGames = 0,

View file

@ -2,8 +2,6 @@ package lila
package user
import com.mongodb.casbah.MongoCollection
import com.mongodb.casbah.Imports.ObjectId
import com.mongodb.DBRef
import chess.EloCalculator
import game.GameRepo
@ -12,16 +10,14 @@ import core.Settings
final class UserEnv(
settings: Settings,
mongodb: String MongoCollection,
gameRepo: GameRepo,
dbRef: String ObjectId DBRef) {
gameRepo: GameRepo) {
import settings._
lazy val historyRepo = new HistoryRepo(mongodb(MongoCollectionHistory))
lazy val userRepo = new UserRepo(
collection = mongodb(MongoCollectionUser),
dbRef = user dbRef(MongoCollectionUser)(user.id))
collection = mongodb(MongoCollectionUser))
lazy val paginator = new PaginatorBuilder(
userRepo = userRepo,

View file

@ -4,7 +4,6 @@ package user
import core.CoreEnv
import controllers.routes
import com.mongodb.casbah.Imports.ObjectId
import play.api.templates.Html
trait UserHelper {
@ -14,20 +13,18 @@ trait UserHelper {
private def cached = env.user.cached
private def usernameMemo = env.user.usernameMemo
def userIdToUsername(userId: String): String = cached username userId
def userIdToUsername(userId: String): String =
cached usernameOrAnonymous userId
def userIdToUsername(userId: Option[String]): String =
userId.fold(userIdToUsername, User.anonymous)
def isUsernameOnline(username: String) = usernameMemo get username
def isUserIdOnline(userId: String) =
usernameMemo get userIdToUsername(userId)
def userIdLink(
userId: Option[String],
cssClass: Option[String]): Html = Html {
(userId map userIdToUsername).fold(
(userId flatMap cached.username).fold(
username """<a class="user_link%s%s" href="%s">%s</a>""".format(
isUsernameOnline(username).fold(" online", ""),
cssClass.fold(" " + _, ""),
@ -38,8 +35,8 @@ trait UserHelper {
}
def userIdLink(
userId: ObjectId,
cssClass: Option[String]): Html = userIdLink(userId.toString.some, cssClass)
userId: String,
cssClass: Option[String]): Html = userIdLink(userId.some, cssClass)
def userLink(
user: User,

View file

@ -3,7 +3,6 @@ package user
import com.novus.salat._
import com.novus.salat.dao._
import com.mongodb.DBRef
import com.mongodb.casbah.{ MongoCollection, WriteConcern }
import com.mongodb.casbah.Imports._
import scalaz.effects._
@ -11,51 +10,38 @@ import com.roundeights.hasher.Implicits._
import ornicar.scalalib.OrnicarRandom
class UserRepo(
collection: MongoCollection,
val dbRef: User DBRef) extends SalatDAO[User, ObjectId](collection) {
collection: MongoCollection
) extends SalatDAO[User, String](collection) {
private val enabledQuery = DBObject("enabled" -> true)
val enabledQuery = DBObject("enabled" -> true)
def byIdQuery(id: String): DBObject = DBObject("_id" -> normalize(id))
def byIdQuery(user: User): DBObject = byIdQuery(user.id)
private def byUsernameQuery(username: String) =
DBObject("usernameCanonical" -> username.toLowerCase)
def normalize(id: String) = id.toLowerCase
private def byIdQuery(id: String) = DBObject("_id" -> new ObjectId(id))
def byId(id: String): IO[Option[User]] = io {
findOneByID(normalize(id))
}
def byId(userId: String): IO[Option[User]] = byId(new ObjectId(userId))
def byId(userId: ObjectId): IO[Option[User]] = io {
findOneByID(userId)
def byIds(ids: Iterable[String]): IO[List[User]] = io {
find("_id" $in ids.map(normalize)).toList
}
def username(userId: String): IO[Option[String]] = io {
primitiveProjection[String](byIdQuery(userId), "username")
}
def id(username: String): IO[Option[ObjectId]] = io {
primitiveProjection[ObjectId](byUsernameQuery(username), "_id")
}
def byUsername(username: String): IO[Option[User]] = io {
findOne(byUsernameQuery(username))
}
def byUsernames(usernames: Iterable[String]): IO[List[User]] = io {
find("usernameCanonical" $in usernames).toList
}
def rank(user: User): IO[Int] = io {
count("elo" $gt user.elo).toInt + 1
}
def setElo(userId: ObjectId, elo: Int): IO[Unit] = io {
collection.update(
idSelector(userId),
$set("elo" -> elo))
def setElo(id: String, elo: Int): IO[Unit] = io {
collection.update(byIdQuery(id), $set("elo" -> elo))
}
def incNbGames(userId: String, rated: Boolean): IO[Unit] = io {
def incNbGames(id: String, rated: Boolean): IO[Unit] = io {
collection.update(
DBObject("_id" -> new ObjectId(userId)),
byIdQuery(id),
if (rated) $inc("nbGames" -> 1, "nbRatedGames" -> 1)
else $inc("nbGames" -> 1))
}
@ -66,30 +52,26 @@ class UserRepo(
}
def toggleChatBan(user: User): IO[Unit] = io {
collection.update(
idSelector(user),
$set("isChatBan" -> !user.isChatBan))
collection.update(byIdQuery(user), $set("isChatBan" -> !user.isChatBan))
}
def saveSetting(user: User, key: String, value: String) = io {
collection.update(
idSelector(user),
$set(("settings." + key) -> value))
collection.update(byIdQuery(user), $set(("settings." + key) -> value))
}
def exists(username: String): IO[Boolean] = io {
count(byUsernameQuery(username)) != 0
count(byIdQuery(username)) != 0
}
def authenticate(username: String, password: String): IO[Option[User]] = for {
userOption byUsername(username)
userOption byId(username)
greenLight authenticable(username, password)
} yield userOption filter (_ greenLight)
private def authenticable(username: String, password: String): IO[Boolean] = io {
for {
data collection.findOne(
byUsernameQuery(username),
byIdQuery(username),
DBObject("password" -> true, "salt" -> true, "enabled" -> true)
)
hashed data.getAs[String]("password")
@ -105,8 +87,8 @@ class UserRepo(
io {
val salt = OrnicarRandom nextAsciiString 32
val obj = DBObject(
"_id" -> normalize(username),
"username" -> username,
"usernameCanonical" -> username.toLowerCase,
"password" -> hash(password, salt),
"salt" -> salt,
"elo" -> User.STARTING_ELO,
@ -115,18 +97,18 @@ class UserRepo(
"enabled" -> true,
"roles" -> Nil)
collection.insert(obj, WriteConcern.Safe)
} flatMap { _ byUsername(username) }
} flatMap { _ byId(username) }
)
} yield userOption
val countEnabled: IO[Int] = io { count(enabledQuery).toInt }
def usernamesLike(username: String): IO[List[String]] = io {
val regex = "^" + username.toLowerCase + ".*$"
val regex = "^" + normalize(username) + ".*$"
collection.find(
DBObject("usernameCanonical" -> regex.r),
DBObject("_id" -> regex.r),
DBObject("username" -> 1))
.sort(DBObject("usernameCanonical" -> 1))
.sort(DBObject("_id" -> 1))
.limit(10)
.toList
.map(_.expand[String]("username"))
@ -150,18 +132,14 @@ class UserRepo(
def disable(user: User) = updateIO(user)($set("enabled" -> false))
def updateIO(username: String)(op: User DBObject): IO[Unit] = for {
userOption byUsername(username)
userOption byId(username)
_ userOption.fold(user updateIO(user)(op(user)), io())
} yield ()
def updateIO(user: User)(obj: DBObject): IO[Unit] = io {
update(idSelector(user), obj)
update(byIdQuery(user), obj)
}
private def idSelector(user: User) = DBObject("_id" -> user.id)
private def idSelector(id: ObjectId) = DBObject("_id" -> id)
private def hash(pass: String, salt: String): String =
"%s{%s}".format(pass, salt).sha1
}

7
bin/migrate Executable file
View file

@ -0,0 +1,7 @@
#!/bin/sh
mongo lichess mongo_migration_user.js
mongo lichess mongo_migration_forum.js
mongo lichess mongo_migration_message.js
mongo lichess mongo_migration_rest.js
mongo lichess mongo_migration_game.js
bin/cli forum-denormalize

View file

@ -5,10 +5,4 @@ import scalaz.effects._
case class Games(gameRepo: GameRepo) {
def index: IO[Unit] = for {
_ putStrLn("- Drop indexes")
_ gameRepo.dropIndexes
_ putStrLn("- Ensure indexes")
_ gameRepo.ensureIndexes
} yield ()
}

View file

@ -25,7 +25,6 @@ object Main {
val op: IO[Unit] = args.toList match {
case "average-elo" :: Nil infos.averageElo
case "game-index" :: Nil games.index
case "trans-js-dump" :: Nil TransJsDump(
path = new File(env.app.path.getCanonicalPath + "/public/trans"),
pool = env.i18n.pool,

View file

@ -18,7 +18,7 @@ case class Users(userRepo: UserRepo, securityStore: Store) {
def perform(username: String, action: String, op: User IO[Unit]) = for {
_ putStrLn(action + " " + username)
userOption userRepo byUsername username
userOption userRepo byId username
_ userOption.fold(
u op(u) flatMap { _ putStrLn("Success") },
putStrLn("Not found")

View file

@ -3,8 +3,8 @@ mongo {
port = 27017
dbName = lichess
collection {
game = game
user = user
game = game2
user = user2
hook = hook
entry = lobby_entry
message = lobby_room

View file

@ -1,8 +1,12 @@
print("Hashing users")
var users = {};
var userHash = {};
db.user2.find({},{oid:1}).forEach(function(user) {
users[user.oid.toString()] = user._id;
userHash[user.oid.toString()] = user._id;
});
function user(oid) {
if(userHash[oid]) return userHash[oid];
throw "Missing user " + oid;
}
var categSlugs = {};
var topicIds = {};
@ -73,7 +77,7 @@ var topicIds = {};
number: obj.number
};
if (obj.author) {
post.userId = users[obj.author['$id'].toString()];
post.userId = user(obj.author['$id'].toString());
}
coll.insert(post);
}

View file

@ -10,11 +10,12 @@ function user(oid) {
print("Games");
var batch = 10000;
var max = 100000;
var oGames = db.game;
var nGames = db.game2;
var it = 0, totalNb = oGames.count();
nGames.drop();
oGames.find().batchSize(batch).limit(100000).forEach(function(game) {
oGames.find().batchSize(batch).limit(max).forEach(function(game) {
delete game["positionHashes"];
delete game["players.0.previousMoveTs"];
delete game["players.1.previousMoveTs"];
@ -45,5 +46,15 @@ oGames.find().batchSize(batch).limit(100000).forEach(function(game) {
}
nGames.insert(game);
++it;
if (0 == it % batch) print(it + "/" + totalNb);
if (0 == it % batch)
print(it + "/" + totalNb + " " + Math.round(100*it/totalNb) + "%");
});
print("Indexes");
nGames.ensureIndex({status:1});
nGames.ensureIndex({userIds:1});
nGames.ensureIndex({winId:1});
nGames.ensureIndex({turns:1});
nGames.ensureIndex({updatedAt:-1});
nGames.ensureIndex({createdAt:-1});
nGames.ensureIndex({userIds:1, createdAt:-1});

View file

@ -1,8 +1,12 @@
print("Hashing users")
var users = {};
var userHash = {};
db.user2.find({},{oid:1}).forEach(function(user) {
users[user.oid.toString()] = user._id;
userHash[user.oid.toString()] = user._id;
});
function user(oid) {
if(userHash[oid]) return userHash[oid];
throw "Missing user " + oid;
}
print("Threads and messages");
var oThreads = db.message_thread;
@ -16,7 +20,7 @@ oThreads.find().forEach(function(oThread) {
name: oThread.subject,
createdAt: oThread.createdAt,
creatorId: creatorId(oThread),
invitedId: users[invOid],
invitedId: user(invOid),
visibleByUserIds: visibleByUserIds(oThread)
};
var posts = [];
@ -57,7 +61,7 @@ function invitedOid(oThread) {
}
function userIds(oThread) {
return [creatorId(oThread), users[invitedOid(oThread)]];
return [creatorId(oThread), user(invitedOid(oThread))];
}
function visibleByUserIds(oThread) {
@ -69,8 +73,8 @@ function visibleByUserIds(oThread) {
}
function username(obj) {
if (typeof obj == "object") return users[objId(obj)];
return users[obj];
if (typeof obj == "object") return user(objId(obj));
return user(obj);
}
function objId(obj) {

13
mongo_migration_rest.js Normal file
View file

@ -0,0 +1,13 @@
print("Other collections");
db.lobby_room.drop();
db.createCollection("lobby_room", {capped:true, max:1000})
db.lobby_entry.drop();
db.createCollection("lobby_entry", {capped:true, max:50})
db.hook.drop();
db.hook.ensureIndex({ownerId: 1});
db.hook.ensureIndex({mode: 1});
db.hook.ensureIndex({createdAt: -1});
db.hook.ensureIndex({match: 1});

6
todo
View file

@ -22,12 +22,10 @@ http://codetunes.com/2012/05/09/scala-dsl-tutorial-writing-web-framework-router
use POST instead of GET where it makes sense
endgame sound http://en.lichess.org/forum/lichess-feedback/checkmate-sound-feature?page=1#1
cached username option app/user/Cached.scala
game.next dbref => string
next deploy:
mongo lichess mongo_migration_user.js
mongo lichess mongo_migration_forum.js
mongo lichess mongo_migration_message.js
bin/cli forum-denormalize
bin/migrate
new translations:
-rematchOfferCanceled=Rematch offer canceled
-rematchOfferDeclined=Rematch offer declined