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:
commit
3def57e60b
|
@ -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
|
||||
|
|
|
@ -283,6 +283,9 @@ push {
|
|||
url = "https://android.googleapis.com/gcm/send"
|
||||
key = ""
|
||||
}
|
||||
apple {
|
||||
password = ""
|
||||
}
|
||||
}
|
||||
mod {
|
||||
collection {
|
||||
|
|
BIN
conf/zpns.p12
Normal file
BIN
conf/zpns.p12
Normal file
Binary file not shown.
|
@ -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")()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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]] =
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
52
modules/push/src/main/ApplePush.scala
Normal file
52
modules/push/src/main/ApplePush.scala
Normal 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!")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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") }
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue