Skip to content

Commit

Permalink
invite n00bs and patzers to compete in limited tournaments - WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
ornicar committed Jun 19, 2016
1 parent 9f0025e commit 1c048f0
Show file tree
Hide file tree
Showing 13 changed files with 123 additions and 43 deletions.
5 changes: 5 additions & 0 deletions app/controllers/Tournament.scala
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,11 @@ object Tournament extends LilaController {
}
}

def limitedInvitation = Auth { implicit ctx =>
me =>
???
}

def websocket(id: String, apiVersion: Int) = SocketOption[JsValue] { implicit ctx =>
get("sri") ?? { uid =>
env.socketHandler.join(id, uid, ctx.me)
Expand Down
1 change: 1 addition & 0 deletions conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ GET /tournament/$id<\w{8}>/show/:user/:nb controllers.Tournament.userGameNbS
GET /tournament/$id<\w{8}>/player/:user controllers.Tournament.player(id: String, user: String)
POST /tournament/$id<\w{8}>/terminate controllers.Tournament.terminate(id: String)
GET /tournament/help controllers.Tournament.help(system: Option[String] ?= None)
GET /tournament/limited-invitation controllers.Tournament.limitedInvitation

# Tournament CRUD
GET /tournament/manager controllers.TournamentCrud.index
Expand Down
41 changes: 18 additions & 23 deletions modules/notify/src/main/BSONHandlers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -59,37 +59,31 @@ private object BSONHandlers {

private def writeNotificationType(notificationContent: NotificationContent) = {
notificationContent match {
case _: MentionedInThread => "mention"
case _: InvitedToStudy => "invitedStudy"
case _: PrivateMessage => "privateMessage"
case _: QaAnswer => "qaAnswer"
case _: TeamJoined => "teamJoined"
case _: NewBlogPost => "newBlogPost"
case _: AnalysisFinished => "analysisFinished"
case _: MentionedInThread => "mention"
case _: InvitedToStudy => "invitedStudy"
case _: PrivateMessage => "privateMessage"
case _: QaAnswer => "qaAnswer"
case _: TeamJoined => "teamJoined"
case _: NewBlogPost => "newBlogPost"
case _: AnalysisFinished => "analysisFinished"
case LimitedTournamentInvitation => "u"
}
}

private def writeNotificationContent(notificationContent: NotificationContent) = {
notificationContent match {
case MentionedInThread(mentionedBy, topic, topicId, category, postId) =>
$doc("type" -> writeNotificationType(notificationContent), "mentionedBy" -> mentionedBy,
"topic" -> topic, "topicId" -> topicId, "category" -> category, "postId" -> postId)
$doc("mentionedBy" -> mentionedBy, "topic" -> topic, "topicId" -> topicId, "category" -> category, "postId" -> postId)
case InvitedToStudy(invitedBy, studyName, studyId) =>
$doc("type" -> writeNotificationType(notificationContent),
"invitedBy" -> invitedBy,
"studyName" -> studyName,
"studyId" -> studyId)
case p: PrivateMessage => PrivateMessageHandler.write(p) ++
$doc("type" -> writeNotificationType(notificationContent))
case q: QaAnswer => QaAnswerHandler.write(q) ++
$doc("type" -> writeNotificationType(notificationContent))
case t: TeamJoined => TeamJoinedHandler.write(t) ++ $doc("type" -> writeNotificationType(notificationContent))
case b: NewBlogPost =>
NewBlogPostHandler.write(b) ++ $doc("type" -> writeNotificationType(notificationContent))
case a: AnalysisFinished =>
AnalysisFinishedHandler.write(a) ++ $doc("type" -> writeNotificationType(notificationContent))
$doc("invitedBy" -> invitedBy, "studyName" -> studyName, "studyId" -> studyId)
case p: PrivateMessage => PrivateMessageHandler.write(p)
case q: QaAnswer => QaAnswerHandler.write(q)
case t: TeamJoined => TeamJoinedHandler.write(t)
case b: NewBlogPost => NewBlogPostHandler.write(b)
case a: AnalysisFinished => AnalysisFinishedHandler.write(a)
case LimitedTournamentInvitation => $empty
}
}
} ++ $doc("type" -> writeNotificationType(notificationContent))

private def readMentionedNotification(reader: Reader): MentionedInThread = {
val mentionedBy = reader.get[MentionedBy]("mentionedBy")
Expand Down Expand Up @@ -117,6 +111,7 @@ private object BSONHandlers {
case "teamJoined" => TeamJoinedHandler read reader.doc
case "newBlogPost" => NewBlogPostHandler read reader.doc
case "analysisFinished" => AnalysisFinishedHandler read reader.doc
case "u" => LimitedTournamentInvitation
}

def writes(writer: Writer, n: NotificationContent): dsl.Bdoc = {
Expand Down
16 changes: 9 additions & 7 deletions modules/notify/src/main/JsonHandlers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,22 @@ final class JSONHandlers(
"id" -> id.value,
"color" -> color.name,
"opponentName" -> against.value)
case LimitedTournamentInvitation => Json.obj()
}
}

def writes(notification: Notification) = {
val body = notification.content

val notificationType = body match {
case _: MentionedInThread => "mentioned"
case _: InvitedToStudy => "invitedStudy"
case _: PrivateMessage => "privateMessage"
case _: QaAnswer => "qaAnswer"
case _: TeamJoined => "teamJoined"
case _: NewBlogPost => "newBlogPost"
case _: AnalysisFinished => "analysisFinished"
case _: MentionedInThread => "mentioned"
case _: InvitedToStudy => "invitedStudy"
case _: PrivateMessage => "privateMessage"
case _: QaAnswer => "qaAnswer"
case _: TeamJoined => "teamJoined"
case _: NewBlogPost => "newBlogPost"
case _: AnalysisFinished => "analysisFinished"
case LimitedTournamentInvitation => "limitedTournamentInvitation"
}

Json.obj("content" -> writeBody(body),
Expand Down
2 changes: 2 additions & 0 deletions modules/notify/src/main/Notification.scala
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,5 @@ object AnalysisFinished {
case class Id(value: String) extends AnyVal with StringValue
case class OpponentName(value: String) extends AnyVal with StringValue
}

case object LimitedTournamentInvitation extends NotificationContent
7 changes: 5 additions & 2 deletions modules/notify/src/main/NotificationRepo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ private final class NotificationRepo(val coll: Coll) {

private val hasOld = $doc(
"read" -> false,
"createdAt" -> $doc("$gt" -> DateTime.now.minusDays(3)))
"createdAt" $gt DateTime.now.minusDays(3))
private val hasUnread = $doc( // recent, read
"createdAt" -> $doc("$gt" -> DateTime.now.minusMinutes(10)))
"createdAt" $gt DateTime.now.minusMinutes(10))
private val hasOldOrUnread =
$doc("$or" -> List(hasOld, hasUnread))

Expand Down Expand Up @@ -59,6 +59,9 @@ private final class NotificationRepo(val coll: Coll) {
) ++ hasOldOrUnread)
}

def exists(notifies: Notification.Notifies, selector: Bdoc): Fu[Boolean] =
coll.exists(userNotificationsQuery(notifies) ++ selector)

val recentSort = $sort desc "createdAt"

def userNotificationsQuery(userId: Notification.Notifies) = $doc("notifies" -> userId)
Expand Down
6 changes: 3 additions & 3 deletions modules/notify/src/main/NotifyApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ final class NotifyApi(
def remove(notifies: Notification.Notifies, selector: Bdoc): Funit =
repo.remove(notifies, selector) >> unreadCountCache.remove(notifies)

def exists = repo.exists _

private def shouldSkip(notification: Notification) =
UserRepo.isKid(notification.notifies.value) flatMap {
case true => fuccess(true)
Expand All @@ -64,9 +66,7 @@ final class NotifyApi(
case InvitedToStudy(invitedBy, _, studyId) => repo.hasRecentStudyInvitation(notification.notifies, studyId)
case PrivateMessage(_, thread, _) => repo.hasRecentPrivateMessageFrom(notification.notifies, thread)
case QaAnswer(_, question, _) => repo.hasRecentQaAnswer(notification.notifies, question)
case _: TeamJoined => fuccess(false)
case _: NewBlogPost => fuccess(false)
case _: AnalysisFinished => fuccess(false)
case _ => fuccess(false)
}
}

Expand Down
4 changes: 4 additions & 0 deletions modules/tournament/src/main/Env.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ final class Env(
onStart: String => Unit,
historyApi: lila.history.HistoryApi,
trophyApi: lila.user.TrophyApi,
notifyApi: lila.notify.NotifyApi,
scheduler: lila.common.Scheduler) {

private val startsAtMillis = nowMillis
Expand Down Expand Up @@ -140,6 +141,8 @@ final class Env(

TournamentScheduler.start(system, api)

TournamentNotifier.start(system, api, notifyApi)

def version(tourId: String): Fu[Int] =
socketHub ? Ask(tourId, GetVersion) mapTo manifest[Int]

Expand Down Expand Up @@ -181,5 +184,6 @@ object Env {
onStart = lila.game.Env.current.onStart,
historyApi = lila.history.Env.current.api,
trophyApi = lila.user.Env.current.trophyApi,
notifyApi = lila.notify.Env.current.api,
scheduler = lila.common.PlayApp.scheduler)
}
63 changes: 63 additions & 0 deletions modules/tournament/src/main/TournamentNotifier.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package lila.tournament

import lila.user.User

import akka.actor._
import org.joda.time.DateTime
import scala.concurrent.duration._

import lila.db.dsl._
import lila.notify.{ Notification, NotifyApi, LimitedTournamentInvitation }
import lila.rating.PerfType

private final class TournamentNotifier private (
api: TournamentApi,
notifyApi: NotifyApi) extends Actor {

import TournamentNotifier._

val minGames = 20
val maxRating = 1700
val perfs = List(PerfType.Blitz, PerfType.Classical)

def receive = {

case User.Active(user) if qualifies(user) =>
notifyApi.exists(Notification.Notifies(user.id), $doc("content.type" -> "u")) flatMap {
case true => funit
case false => notifyApi addNotification Notification(
Notification.Notifies(user.id),
LimitedTournamentInvitation)
}
}

def qualifies(user: User) = {
println(user.seenRecently, user.id)
println {
!user.seenRecently &&
!user.kid &&
user.count.rated > 50 &&
user.toints < 100 &&
user.perfs.bestRatingInWithMinGames(perfs, minGames).??(maxRating >=) &&
!alreadyNotified(user)
}
!user.seenRecently
}

def alreadyNotified(user: User) =
if (notifiedCache get user.id) false
else {
notifiedCache put user.id
true
}

val notifiedCache = new lila.memo.ExpireSetMemo(1 hour)
}

private object TournamentNotifier {

def start(system: ActorSystem, api: TournamentApi, notifyApi: NotifyApi) = {
val ref = system.actorOf(Props(new TournamentNotifier(api, notifyApi)))
system.lilaBus.subscribe(ref, 'userActive)
}
}
6 changes: 6 additions & 0 deletions modules/user/src/main/Perfs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ case class Perfs(
} | Perf.default.intRating
}

def bestRatingInWithMinGames(types: List[PerfType], nbGames: Int): Option[Int] =
types.map(apply).foldLeft(none[Int]) {
case (ro, p) if p.nb >= nbGames && ro.fold(true)(_ < p.intRating) => p.intRating.some
case (ro, _) => ro
}

def bestProgress: Int = bestProgressIn(PerfType.leaderboardable)

def bestProgressIn(types: List[PerfType]): Int = types map apply match {
Expand Down
2 changes: 1 addition & 1 deletion modules/user/src/main/User.scala
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ case class User(

def hasTitle = title.isDefined

def seenRecently: Boolean = timeNoSee < 2.minutes
lazy val seenRecently: Boolean = timeNoSee < 2.minutes

def timeNoSee: Duration = seenAt.fold[Duration](Duration.Inf) { s =>
(nowMillis - s.getMillis).millis
Expand Down
2 changes: 1 addition & 1 deletion project/Build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ object ApplicationBuild extends Build {
)

lazy val tournament = project("tournament", Seq(
common, hub, socket, chess, game, round, security, chat, memo, quote, history)).settings(
common, hub, socket, chess, game, round, security, chat, memo, quote, history, notifyModule)).settings(
libraryDependencies ++= provided(play.api, RM)
)

Expand Down
11 changes: 5 additions & 6 deletions ui/notify/src/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,17 +129,16 @@ var handlers = {
return n.content.title;
}
},
analysisFinished: {
limitedTournamentInvitation: {
html: function(notification) {
var content = notification.content
var url = "/" + content.id + "/" + content.color
var url = '/tournament/limited-invitation';

return genericNotification(notification, url, 'A', [
return genericNotification(notification, url, 'g', [
m('span', [
m('strong', 'Analysis complete'),
m('strong', 'New rating limited tournaments!'),
drawTime(notification)
]),
m('span', 'Analysis of game against « ' + content.opponentName + ' » complete.')
m('span', 'Experience lichess arena tournaments with players of your level')
]);
},
text: function(n) {
Expand Down

0 comments on commit 1c048f0

Please sign in to comment.