Merge branch 'ios-push-rm-cursor' into ios-push-rm-cursor-fishnet

* ios-push-rm-cursor:
  send iOS notifications with pushy-scala
  resolve RM & PRM from local repository because it's much faster
  use my own maven repo for RM and PRM
  RM 0.11.9-SNAPSHOT with secondary cursor kill fix
  Revert "remove all read preferences"
  so yeah, dependency that changes with JVM update version, fuck that
  implement iOS mobile push notifications
This commit is contained in:
Thibault Duplessis 2016-03-15 01:44:42 +07:00
commit 3def57e60b
17 changed files with 178 additions and 84 deletions

View file

@ -1,10 +1,21 @@
#!/bin/sh
set -e
mkdir -p local
cd local
rm -rf maven
git clone https://github.com/ornicar/maven
cd ..
dir=$(mktemp -d)
echo "Building in $dir"
cd "$dir"
git clone https://github.com/msimav/pushy-scala
cd pushy-scala
sbt publish-local
cd ..
git clone https://github.com/ornicar/scalalib
cd scalalib
sbt publish-local

View file

@ -283,6 +283,9 @@ push {
url = "https://android.googleapis.com/gcm/send"
key = ""
}
apple {
password = ""
}
}
mod {
collection {

BIN
conf/zpns.p12 Normal file

Binary file not shown.

View file

@ -218,11 +218,11 @@ object mon {
def out = inc(s"push.register.out")
}
object send {
val move = inc("push.send.move")
val finish = inc("push.send.finish")
def move(platform: String) = inc(s"push.send.$platform.move")()
def finish(platform: String) = inc(s"push.send.$platform.finish")()
object challenge {
val create = inc("push.send.challenge.create")
val accept = inc("push.send.challenge.accept")
def create(platform: String) = inc(s"push.send.$platform.challenge_create")()
def accept(platform: String) = inc(s"push.send.$platform.challenge_accept")()
}
}
}

View file

@ -12,13 +12,15 @@ import lila.common.paginator.AdapterLike
final class Adapter[A: TubeInColl](
selector: JsObject,
sort: Sort) extends AdapterLike[A] {
sort: Sort,
readPreference: ReadPreference = ReadPreference.primary) extends AdapterLike[A] {
def nbResults: Fu[Int] = $count(selector)
def slice(offset: Int, length: Int): Fu[Seq[A]] = $find(
pimpQB($query(selector)).sort(sort: _*) skip offset,
length)
length,
readPreference = readPreference)
}
final class CachedAdapter[A](
@ -33,7 +35,8 @@ final class BSONAdapter[A: BSONDocumentReader](
collection: BSONCollection,
selector: BSONDocument,
projection: BSONDocument,
sort: BSONDocument) extends AdapterLike[A] {
sort: BSONDocument,
readPreference: ReadPreference = ReadPreference.primary) extends AdapterLike[A] {
def nbResults: Fu[Int] = collection.count(Some(selector))
@ -41,6 +44,6 @@ final class BSONAdapter[A: BSONDocumentReader](
collection.find(selector, projection)
.sort(sort)
.copy(options = QueryOpts(skipN = offset))
.cursor[A]()
.cursor[A](readPreference = readPreference)
.collect[List](length)
}

View file

@ -51,4 +51,7 @@ object $find {
def apply[A: TubeInColl](b: QueryBuilder, nb: Int): Fu[List[A]] =
b.toList[Option[A]](nb.some) map (_.flatten)
def apply[A: TubeInColl](b: QueryBuilder, nb: Int, readPreference: ReadPreference): Fu[List[A]] =
b.toList[Option[A]](nb.some, readPreference) map (_.flatten)
}

View file

@ -49,9 +49,9 @@ trait Implicits extends Types {
def batch(nb: Int): QueryBuilder = b.options(b.options batchSize nb)
def toList[A: BSONDocumentReader](limit: Option[Int]): Fu[List[A]] =
limit.fold(b.cursor[A]().collect[List]()) { l =>
batch(l).cursor[A]().collect[List](l)
def toList[A: BSONDocumentReader](limit: Option[Int], readPreference: ReadPreference = ReadPreference.primary): Fu[List[A]] =
limit.fold(b.cursor[A](readPreference = readPreference).collect[List]()) { l =>
batch(l).cursor[A](readPreference = readPreference).collect[List](l)
}
def toListFlatten[A: Tube](limit: Option[Int]): Fu[List[A]] =

View file

@ -51,7 +51,7 @@ private final class ExplorerIndexer(
import reactivemongo.api._
pimpQB(query)
.sort(Query.sortChronological)
.cursor[Game]()
.cursor[Game](ReadPreference.secondaryPreferred)
.enumerate(maxGames, stopOnError = true) &>
Enumeratee.mapM[Game].apply[Option[GamePGN]] { game =>
makeFastPgn(game) map {

View file

@ -10,6 +10,8 @@ import tube.gameTube
private[game] final class PaginatorBuilder(cached: Cached, maxPerPage: Int) {
private val readPreference = reactivemongo.api.ReadPreference.secondaryPreferred
def recentlyCreated(selector: JsObject, nb: Option[Int] = None) =
apply(selector, Seq(Query.sortCreated), nb) _
@ -29,7 +31,8 @@ private[game] final class PaginatorBuilder(cached: Cached, maxPerPage: Int) {
private def noCacheAdapter(selector: JsObject, sort: Sort): AdapterLike[Game] =
new Adapter(
selector = selector,
sort = sort)
sort = sort,
readPreference = readPreference)
private def paginator(adapter: AdapterLike[Game], page: Int): Fu[Paginator[Game]] =
Paginator(adapter, currentPage = page, maxPerPage = maxPerPage)

View file

@ -0,0 +1,52 @@
package lila.push
import akka.actor._
import java.io.InputStream
import scala.util.Failure
import com.vngrs.scala.pushy._
import com.vngrs.scala.pushy.Implicits._
import play.api.libs.json._
private final class ApplePush(
getDevice: String => Fu[Option[Device]],
system: ActorSystem,
certificate: InputStream,
password: String) {
private val actor = system.actorOf(Props(classOf[ApnsActor], certificate, password))
def apply(userId: String)(data: => PushApi.Data): Funit =
getDevice(userId) map {
_ foreach { device =>
val token = device.id
val payload = Payload(Json stringify Json.obj(
"alert" -> Json.obj(
"title" -> data.title,
"body" -> data.body
),
"data" -> data.payload))
actor ! PushNotification(token, payload)
}
}
}
// the damn API is blocking, so at least use only one thread at a time
private final class ApnsActor(certificate: InputStream, password: String) extends Actor {
val logger = play.api.Logger("push")
var manager: PushManager = _
override def preStart {
manager = PushManager.sandbox("Example", SSLContext(certificate, password).get)
}
def receive = {
case notification: PushNotification =>
manager send notification match {
case Failure(ex) => logger.warn(s"iOS notification failed because ${ex.getMessage}!")
case _ => logger.info("iOS notification sent!")
}
}
}

View file

@ -3,7 +3,7 @@ package lila.push
import org.joda.time.DateTime
private final case class Device(
_id: String, // google device ID
_id: String, // google device ID or Apple token
platform: String, // cordova platform (android, ios)
userId: String,
seenAt: DateTime) {

View file

@ -2,6 +2,7 @@ package lila.push
import akka.actor._
import com.typesafe.config.Config
import java.io.InputStream
import lila.common.PimpedConfig._
@ -11,11 +12,13 @@ final class Env(
getLightUser: String => Option[lila.common.LightUser],
isOnline: lila.user.User.ID => Boolean,
roundSocketHub: ActorSelection,
appleCertificate: InputStream,
system: ActorSystem) {
private val CollectionDevice = config getString "collection.device"
private val GooglePushUrl = config getString "google.url"
private val GooglePushKey = config getString "google.key"
private val ApplePushPassword = config getString "apple.password"
private lazy val deviceApi = new DeviceApi(db(CollectionDevice))
@ -27,8 +30,15 @@ final class Env(
url = GooglePushUrl,
key = GooglePushKey)
private lazy val applePush = new ApplePush(
deviceApi.findLastByUserId _,
system = system,
certificate = appleCertificate,
password = ApplePushPassword)
private lazy val pushApi = new PushApi(
googlePush,
applePush,
getLightUser,
isOnline,
roundSocketHub)
@ -55,5 +65,8 @@ object Env {
getLightUser = lila.user.Env.current.lightUser,
isOnline = lila.user.Env.current.isOnline,
roundSocketHub = lila.hub.Env.current.socket.round,
appleCertificate = lila.common.PlayApp.withApp {
_.classloader.getResourceAsStream("zpns.p12")
},
config = lila.common.PlayApp loadConfig "push")
}

View file

@ -9,7 +9,7 @@ private final class GooglePush(
url: String,
key: String) {
def apply(userId: String)(data: => GooglePush.Data): Funit =
def apply(userId: String)(data: => PushApi.Data): Funit =
getDevice(userId) flatMap {
_ ?? { device =>
WS.url(url)
@ -32,11 +32,3 @@ private final class GooglePush(
}
}
}
private object GooglePush {
case class Data(
title: String,
body: String,
payload: JsObject)
}

View file

@ -14,6 +14,7 @@ import play.api.libs.json._
private final class PushApi(
googlePush: GooglePush,
applePush: ApplePush,
implicit val lightUser: String => Option[LightUser],
isOnline: User.ID => Boolean,
roundSocketHub: ActorSelection) {
@ -23,27 +24,23 @@ private final class PushApi(
else game.userIds.map { userId =>
Pov.ofUserId(game, userId) ?? { pov =>
IfAway(pov) {
googlePush(userId) {
lila.mon.push.send.finish()
GooglePush.Data(
title = pov.win match {
case Some(true) => "You won!"
case Some(false) => "You lost."
case _ => "It's a draw."
},
body = s"Your game with ${opponentName(pov)} is over.",
payload = Json.obj(
"userId" -> userId,
"userData" -> Json.obj(
"type" -> "gameFinish",
"gameId" -> game.id,
"fullId" -> pov.fullId,
"color" -> pov.color.name,
"fen" -> Forsyth.exportBoard(game.toChess.board),
"lastMove" -> game.castleLastMoveTime.lastMoveString,
"win" -> pov.win)
))
}
pushToAll(userId, _.finish, PushApi.Data(
title = pov.win match {
case Some(true) => "You won!"
case Some(false) => "You lost."
case _ => "It's a draw."
},
body = s"Your game with ${opponentName(pov)} is over.",
payload = Json.obj(
"userId" -> userId,
"userData" -> Json.obj(
"type" -> "gameFinish",
"gameId" -> game.id,
"fullId" -> pov.fullId,
"color" -> pov.color.name,
"fen" -> Forsyth.exportBoard(game.toChess.board),
"lastMove" -> game.castleLastMoveTime.lastMoveString,
"win" -> pov.win))))
}
}
}.sequenceFu.void
@ -55,23 +52,19 @@ private final class PushApi(
game.player(!move.color).userId ?? { userId =>
game.pgnMoves.lastOption ?? { sanMove =>
IfAway(pov) {
googlePush(userId) {
lila.mon.push.send.move()
GooglePush.Data(
title = "It's your turn!",
body = s"${opponentName(pov)} played $sanMove",
payload = Json.obj(
"userId" -> userId,
"userData" -> Json.obj(
"type" -> "gameMove",
"gameId" -> game.id,
"fullId" -> pov.fullId,
"color" -> pov.color.name,
"fen" -> Forsyth.exportBoard(game.toChess.board),
"lastMove" -> game.castleLastMoveTime.lastMoveString,
"secondsLeft" -> pov.remainingSeconds)
))
}
pushToAll(userId, _.move, PushApi.Data(
title = "It's your turn!",
body = s"${opponentName(pov)} played $sanMove",
payload = Json.obj(
"userId" -> userId,
"userData" -> Json.obj(
"type" -> "gameMove",
"gameId" -> game.id,
"fullId" -> pov.fullId,
"color" -> pov.color.name,
"fen" -> Forsyth.exportBoard(game.toChess.board),
"lastMove" -> game.castleLastMoveTime.lastMoveString,
"secondsLeft" -> pov.remainingSeconds))))
}
}
}
@ -82,18 +75,14 @@ private final class PushApi(
def challengeCreate(c: Challenge): Funit = c.destUser.filterNot(u => isOnline(u.id)) ?? { dest =>
c.challengerUser ?? { challenger =>
lightUser(challenger.id) ?? { lightChallenger =>
googlePush(dest.id) {
lila.mon.push.send.challenge.create()
GooglePush.Data(
title = s"${lightChallenger.titleName} (${challenger.rating.show}) challenges you!",
body = describeChallenge(c),
payload = Json.obj(
"userId" -> dest.id,
"userData" -> Json.obj(
"type" -> "challengeCreate",
"challengeId" -> c.id))
)
}
pushToAll(dest.id, _.challenge.create, PushApi.Data(
title = s"${lightChallenger.titleName} (${challenger.rating.show}) challenges you!",
body = describeChallenge(c),
payload = Json.obj(
"userId" -> dest.id,
"userData" -> Json.obj(
"type" -> "challengeCreate",
"challengeId" -> c.id))))
}
}
}
@ -101,19 +90,28 @@ private final class PushApi(
def challengeAccept(c: Challenge, joinerId: Option[String]): Funit =
c.challengerUser.ifTrue(c.finalColor.white).filterNot(u => isOnline(u.id)) ?? { challenger =>
val lightJoiner = joinerId flatMap lightUser
googlePush(challenger.id) {
lila.mon.push.send.challenge.accept()
GooglePush.Data(
pushToAll(challenger.id, _.challenge.accept, PushApi.Data(
title = s"${lightJoiner.fold("Anonymous")(_.titleName)} accepts your challenge!",
body = describeChallenge(c),
payload = Json.obj(
"userId" -> challenger.id,
"userData" -> Json.obj(
"type" -> "challengeAccept",
"challengeId" -> c.id))
)
"challengeId" -> c.id))))
}
private type MonitorType = lila.mon.push.send.type => (String => Unit)
private def pushToAll(userId: String, monitor: MonitorType, data: PushApi.Data) = {
googlePush(userId) {
monitor(lila.mon.push.send)("android")
data
}
applePush(userId) {
monitor(lila.mon.push.send)("ios")
data
}
}
private def describeChallenge(c: Challenge) = {
import lila.challenge.Challenge.TimeControl._
@ -138,3 +136,11 @@ private final class PushApi(
private def opponentName(pov: Pov) = Namer playerString pov.opponent
}
private object PushApi {
case class Data(
title: String,
body: String,
payload: JsObject)
}

View file

@ -63,7 +63,7 @@ object UserRepo {
def byIdsSortRating(ids: Iterable[ID], nb: Int) =
coll.find(BSONDocument("_id" -> BSONDocument("$in" -> ids)) ++ goodLadSelectBson)
.sort(BSONDocument(s"perfs.standard.gl.r" -> -1))
.cursor[User]()
.cursor[User](ReadPreference.secondaryPreferred)
.collect[List](nb)
// expensive, send to secondary
@ -72,7 +72,7 @@ object UserRepo {
BSONDocument("_id" -> BSONDocument("$in" -> ids)) ++ goodLadSelectBson,
BSONDocument("_id" -> true))
.sort(BSONDocument(s"perfs.standard.gl.r" -> -1))
.cursor[BSONDocument]()
.cursor[BSONDocument](ReadPreference.secondaryPreferred)
.collect[List](nb).map {
_.flatMap { _.getAs[String]("_id") }
}

View file

@ -35,7 +35,7 @@ object ApplicationBuild extends Build {
scalaz, scalalib, hasher, config, apache,
jgit, findbugs, RM, PRM, akka.actor, akka.slf4j,
spray.caching, maxmind, prismic,
kamon.core, kamon.statsd),
kamon.core, kamon.statsd, pushyScala),
TwirlKeys.templateImports ++= Seq(
"lila.game.{ Game, Player, Pov }",
"lila.tournament.Tournament",
@ -243,7 +243,7 @@ object ApplicationBuild extends Build {
)
lazy val push = project("push", Seq(common, db, user, game, challenge)).settings(
libraryDependencies ++= provided(play.api, RM, PRM)
libraryDependencies ++= provided(play.api, RM, PRM, pushyScala)
)
lazy val slack = project("slack", Seq(common, hub, user)).settings(

View file

@ -4,6 +4,9 @@ import sbt._, Keys._
object Dependencies {
object Resolvers {
private val workingDir = java.nio.file.Paths.get("").toAbsolutePath().toString
val typesafe = "typesafe.com" at "http://repo.typesafe.com/typesafe/releases/"
val sonatype = "sonatype" at "https://oss.sonatype.org/content/repositories/releases"
val sonatypeS = "sonatype snapshots" at "https://oss.sonatype.org/content/repositories/snapshots"
@ -12,9 +15,13 @@ object Dependencies {
val awesomepom = "awesomepom" at "https://raw.github.com/jibs/maven-repo-scala/master"
val sprayRepo = "spray repo" at "http://repo.spray.io"
val prismic = "Prismic.io kits" at "https://s3.amazonaws.com/prismic-maven-kits/repository/maven/"
val ornicarMaven = "ornicar maven" at "https://raw.githubusercontent.com/ornicar/maven/master/oss.sonatype.org/content/repositories/snapshots"
val local = "Local Maven Repository" at s"file:///$workingDir/local/maven/oss.sonatype.org/content/repositories/snapshots"
val commons = Seq(
// sonatypeS,
// ornicarMaven,
local,
sonatype,
awesomepom,
typesafe,
@ -31,10 +38,11 @@ object Dependencies {
val hasher = "com.roundeights" %% "hasher" % "1.2.0"
val jgit = "org.eclipse.jgit" % "org.eclipse.jgit" % "3.2.0.201312181205-r"
val jodaTime = "joda-time" % "joda-time" % "2.9.1"
val RM = "org.reactivemongo" %% "reactivemongo" % "0.11.9"
val PRM = "org.reactivemongo" %% "play2-reactivemongo" % "0.11.9"
val RM = "org.reactivemongo" % "reactivemongo_2.11" % "0.11.9-SNAPSHOT"
val PRM = "org.reactivemongo" % "play2-reactivemongo_2.11" % "0.11.9-SNAPSHOT"
val maxmind = "com.sanoma.cda" %% "maxmind-geoip2-scala" % "1.2.3-THIB"
val prismic = "io.prismic" %% "scala-kit" % "1.2.11-THIB"
val pushyScala = "default" %% "pushy-scala" % "0.1-SNAPSHOT"
object play {
val version = "2.4.6"