migrate plan module

rm0193-mapreduce
Thibault Duplessis 2019-12-01 11:24:02 -06:00
parent 29a411ad38
commit c27821e881
10 changed files with 140 additions and 116 deletions

View File

@ -4,24 +4,37 @@ import akka.actor._
import com.typesafe.config.Config
import play.api.Configuration
object actors {
trait Actor {
val actor: ActorSelection
val ! = actor ! _
}
case class Relation(actor: ActorSelection) extends Actor
case class Timeline(actor: ActorSelection) extends Actor
case class Report(actor: ActorSelection) extends Actor
case class Renderer(actor: ActorSelection) extends Actor
}
final class Env(
appConfig: Configuration,
system: ActorSystem
) {
import actors._
val config = appConfig.get[Config]("hub")
val gameSearch = select("actor.game.search")
val renderer = select("actor.renderer")
val renderer = Renderer(select("actor.renderer"))
val captcher = select("actor.captcher")
val forumSearch = select("actor.forum.search")
val teamSearch = select("actor.team.search")
val fishnet = select("actor.fishnet")
val tournamentApi = select("actor.tournament.api")
val timeline = select("actor.timeline.user")
val timeline = Timeline(select("actor.timeline.user"))
val bookmark = select("actor.bookmark")
val relation = select("actor.relation")
val report = select("actor.report")
val relation = Relation(select("actor.relation"))
val report = Report(select("actor.report"))
val shutup = select("actor.shutup")
val mod = select("actor.mod")
val notification = select("actor.notify")

View File

@ -1,40 +1,43 @@
package lila.plan
import com.typesafe.config.Config
import com.softwaremill.macwire._
import io.methvin.play.autoconfig._
import play.api.libs.ws.WSClient
import lila.memo.SettingStore.{ StringReader, Formable }
import play.api.Configuration
import scala.concurrent.duration._
import lila.common.config._
@Module
private class PlanConfig(
@ConfigName("collection.patron") val patronColl: CollName,
@ConfigName("collection.charge") val chargeColl: CollName,
val stripe: StripeClient.Config,
@ConfigName("paypal.ipn_key") val payPalIpnKey: Secret
)
final class Env(
config: Config,
appConfig: Configuration,
db: lila.db.Env,
hub: lila.hub.Env,
ws: WSClient,
timeline: lila.hub.actors.Timeline,
notifyApi: lila.notify.NotifyApi,
system: akka.actor.ActorSystem,
asyncCache: lila.memo.AsyncCache.Builder,
lightUserApi: lila.user.LightUserApi,
settingStore: lila.memo.SettingStore.Builder,
scheduler: lila.common.Scheduler
) {
userRepo: lila.user.UserRepo,
settingStore: lila.memo.SettingStore.Builder
)(implicit system: akka.actor.ActorSystem) {
val stripePublicKey = config getString "stripe.keys.public"
import StripeClient.configLoader
private val config = appConfig.get[PlanConfig]("plan")(AutoConfig.loader)
private val CollectionPatron = config getString "collection.patron"
private val CollectionCharge = config getString "collection.charge"
private lazy val patronColl = db(config.patronColl)
private lazy val chargeColl = db(config.chargeColl)
private lazy val patronColl = db(CollectionPatron)
private lazy val chargeColl = db(CollectionCharge)
private lazy val stripeClient: StripeClient = wire[StripeClient]
private lazy val stripeClient = new StripeClient(StripeClient.Config(
endpoint = config getString "stripe.endpoint",
publicKey = stripePublicKey,
secretKey = config getString "stripe.keys.secret"
))
private lazy val notifier = new PlanNotifier(
notifyApi = notifyApi,
scheduler = scheduler,
timeline = hub.timeline
)
private lazy val notifier: PlanNotifier = wire[PlanNotifier]
private lazy val monthlyGoalApi = new MonthlyGoalApi(
getGoal = () => Usd(donationGoalSetting.get()),
@ -43,27 +46,32 @@ final class Env(
val donationGoalSetting = settingStore[Int](
"donationGoal",
default = 10 * 1000,
default = 0,
text = "Monthly donation goal in USD from https://lichess.org/costs".some
)
lazy val api = new PlanApi(
stripeClient,
stripeClient = stripeClient,
patronColl = patronColl,
chargeColl = chargeColl,
notifier = notifier,
userRepo = userRepo,
lightUserApi = lightUserApi,
asyncCache = asyncCache,
payPalIpnKey = PayPalIpnKey(config getString "paypal.ipn_key"),
payPalIpnKey = config.payPalIpnKey,
monthlyGoalApi = monthlyGoalApi
)(system)
)
private lazy val webhookHandler = new WebhookHandler(api)
private lazy val expiration = new Expiration(patronColl, notifier)
private lazy val expiration = new Expiration(
userRepo,
patronColl,
notifier
)
scheduler.future(15 minutes, "Expire patron plans") {
expiration.run
system.scheduler.scheduleWithFixedDelay(15 minutes, 15 minutes) {
() => expiration.run
}
def webhook = webhookHandler.apply _
@ -71,27 +79,12 @@ final class Env(
def cli = new lila.common.Cli {
def process = {
case "patron" :: "lifetime" :: user :: Nil =>
lila.user.UserRepo named user flatMap { _ ?? api.setLifetime } inject "ok"
userRepo named user flatMap { _ ?? api.setLifetime } inject "ok"
// someone donated while logged off.
// we cannot bind the charge to the user so they get their precious wings.
// instead, give them a free month.
case "patron" :: "month" :: user :: Nil =>
lila.user.UserRepo named user flatMap { _ ?? api.giveMonth } inject "ok"
userRepo named user flatMap { _ ?? api.giveMonth } inject "ok"
}
}
}
object Env {
lazy val current: Env = "plan" boot new Env(
config = lila.common.PlayApp loadConfig "plan",
db = lila.db.Env.current,
hub = lila.hub.Env.current,
notifyApi = lila.notify.Env.current.api,
lightUserApi = lila.user.Env.current.lightUserApi,
system = lila.common.PlayApp.system,
asyncCache = lila.memo.Env.current.asyncCache,
settingStore = lila.memo.Env.current.settingStore,
scheduler = lila.common.PlayApp.scheduler
)
}

View File

@ -5,23 +5,27 @@ import lila.user.UserRepo
import org.joda.time.DateTime
private final class Expiration(patronColl: Coll, notifier: PlanNotifier) {
private final class Expiration(
userRepo: UserRepo,
patronColl: Coll,
notifier: PlanNotifier
) {
import BsonHandlers._
import PatronHandlers._
def run: Funit = getExpired flatMap {
_.map { patron =>
patronColl.update($id(patron.id), patron.removeStripe.removePayPal) >>
patronColl.update.one($id(patron.id), patron.removeStripe.removePayPal) >>
disableUserPlanOf(patron) >>-
logger.info(s"Expired ${patron}")
}.sequenceFu.void
}
private def disableUserPlanOf(patron: Patron): Funit =
UserRepo byId patron.userId flatMap {
userRepo byId patron.userId flatMap {
_ ?? { user =>
UserRepo.setPlan(user, user.plan.disable) >>
userRepo.setPlan(user, user.plan.disable) >>
notifier.onExpire(user)
}
}

View File

@ -1,7 +1,6 @@
package lila.plan
import org.joda.time.DateTime
import reactivemongo.api.collections.bson.BSONBatchCommands.AggregationFramework._
import reactivemongo.api.bson.BSONNull
import lila.db.dsl._
@ -13,13 +12,14 @@ private final class MonthlyGoalApi(getGoal: () => Usd, chargeColl: Coll) {
}
def monthAmount: Fu[Cents] =
chargeColl.aggregateOne(
Match($doc("date" $gt DateTime.now.withDayOfMonth(1).withTimeAtStartOfDay)), List(
chargeColl.aggregateWith() { framework =>
import framework._
Match($doc("date" $gt DateTime.now.withDayOfMonth(1).withTimeAtStartOfDay)) -> List(
Group(BSONNull)("cents" -> SumField("cents"))
)
).map {
~_.flatMap { _.getAs[Int]("cents") }
} map Cents.apply
}.headOption.map {
~_.flatMap { _.int("cents") }
} dmap Cents.apply
}
case class MonthlyGoal(current: Cents, goal: Cents) {

View File

@ -5,6 +5,7 @@ import reactivemongo.api.collections.bson.BSONBatchCommands.AggregationFramework
import reactivemongo.api.ReadPreference
import scala.concurrent.duration._
import lila.common.config.Secret
import lila.common.{ Bus, Every, AtMost }
import lila.db.dsl._
import lila.memo.PeriodicRefreshCache
@ -15,9 +16,10 @@ final class PlanApi(
patronColl: Coll,
chargeColl: Coll,
notifier: PlanNotifier,
userRepo: UserRepo,
lightUserApi: lila.user.LightUserApi,
asyncCache: lila.memo.AsyncCache.Builder,
payPalIpnKey: PayPalIpnKey,
payPalIpnKey: Secret,
monthlyGoalApi: MonthlyGoalApi
)(implicit system: akka.actor.ActorSystem) {
@ -56,7 +58,7 @@ final class PlanApi(
isLifetime(user).flatMap { lifetime =>
!lifetime ?? setDbUserPlan(user, user.plan.disable)
} >>
patronColl.update($id(user.id), $unset("stripe", "payPal", "expiresAt")).void >>-
patronColl.update.one($id(user.id), $unset("stripe", "payPal", "expiresAt")).void >>-
logger.info(s"Canceled subscription $sub of ${user.username}")
}
}
@ -75,12 +77,12 @@ final class PlanApi(
funit
case Some(patron) =>
logger.info(s"Charged $charge $patron")
UserRepo byId patron.userId flatten s"Missing user for $patron" flatMap { user =>
userRepo byId patron.userId orFail s"Missing user for $patron" flatMap { user =>
val p2 = patron.copy(
stripe = Patron.Stripe(stripeCharge.customer).some,
free = none
).levelUpIfPossible
patronColl.update($id(patron.id), p2) >>
patronColl.update.one($id(patron.id), p2) >>
setDbUserPlanOnCharge(user, p2) >> {
stripeCharge.lifetimeWorthy ?? setLifetime(user)
}
@ -97,9 +99,9 @@ final class PlanApi(
name: Option[String],
txnId: Option[String],
ip: String,
key: PayPalIpnKey
key: String
): Funit =
if (key != payPalIpnKey) {
if (key != payPalIpnKey.value) {
logger.error(s"Invalid PayPal IPN key $key from $ip $userId $cents")
funit
} else if (cents.value < 100) {
@ -118,11 +120,11 @@ final class PlanApi(
cents = cents
)
addCharge(charge) >>
(userId ?? UserRepo.named) flatMap { userOption =>
(userId ?? userRepo.named) flatMap { userOption =>
userOption ?? { user =>
val payPal = Patron.PayPal(email, subId, DateTime.now)
userPatron(user).flatMap {
case None => patronColl.insert(Patron(
case None => patronColl.insert.one(Patron(
_id = Patron.UserId(user.id),
payPal = payPal.some,
lastLevelUp = DateTime.now
@ -134,7 +136,7 @@ final class PlanApi(
payPal = payPal.some,
free = none
).levelUpIfPossible.expireInOneMonth
patronColl.update($id(patron.id), p2) >>
patronColl.update.one($id(patron.id), p2) >>
setDbUserPlanOnCharge(user, p2)
} >> {
charge.lifetimeWorthy ?? setLifetime(user)
@ -160,9 +162,9 @@ final class PlanApi(
logger.info(s"Ignore sub end for lifetime patron $patron")
funit
case Some(patron) =>
UserRepo byId patron.userId flatten s"Missing user for $patron" flatMap { user =>
userRepo byId patron.userId orFail s"Missing user for $patron" flatMap { user =>
setDbUserPlan(user, user.plan.disable) >>
patronColl.update($id(user.id), patron.removeStripe).void >>
patronColl.update.one($id(user.id), patron.removeStripe).void >>
notifier.onExpire(user) >>-
logger.info(s"Unsubed ${user.username} ${sub}")
}
@ -204,10 +206,10 @@ final class PlanApi(
case (Some(stripe), _) => stripeClient.getCustomer(stripe.customerId) flatMap {
case None =>
logger.warn(s"${user.username} sync: unset DB patron that's not in stripe")
patronColl.update($id(patron.id), patron.removeStripe) >> sync(user)
patronColl.update.one($id(patron.id), patron.removeStripe) >> sync(user)
case Some(customer) if customer.firstSubscription.isEmpty =>
logger.warn(s"${user.username} sync: unset DB patron of customer without a subscription")
patronColl.update($id(patron.id), patron.removeStripe) >> sync(user)
patronColl.update.one($id(patron.id), patron.removeStripe) >> sync(user)
case Some(customer) if customer.firstSubscription.isDefined && !user.plan.active =>
logger.warn(s"${user.username} sync: enable plan of customer with a subscription")
setDbUserPlan(user, user.plan.enable) inject ReloadUser
@ -234,11 +236,11 @@ final class PlanApi(
_.exists(_.isLifetime)
}
def setLifetime(user: User): Funit = UserRepo.setPlan(user, lila.user.Plan(
def setLifetime(user: User): Funit = userRepo.setPlan(user, lila.user.Plan(
months = user.plan.months | 1,
active = true,
since = user.plan.since orElse DateTime.now.some
)) >> patronColl.update(
)) >> patronColl.update.one(
$id(user.id),
$set(
"lastLevelUp" -> DateTime.now,
@ -248,11 +250,11 @@ final class PlanApi(
upsert = true
).void >>- lightUserApi.invalidate(user.id)
def giveMonth(user: User): Funit = UserRepo.setPlan(user, lila.user.Plan(
def giveMonth(user: User): Funit = userRepo.setPlan(user, lila.user.Plan(
months = user.plan.months | 1,
active = true,
since = user.plan.since orElse DateTime.now.some
)) >> patronColl.update(
)) >> patronColl.update.one(
$id(user.id),
$set(
"lastLevelUp" -> DateTime.now,
@ -274,8 +276,8 @@ final class PlanApi(
def recentChargeUserIds: Fu[List[User.ID]] = recentChargeUserIdsCache.get
def recentChargesOf(user: User): Fu[List[Charge]] =
chargeColl.find($doc("userId" -> user.id)).sort($doc("date" -> -1)).list[Charge]()
def recentCharesOf(user: User): Fu[List[Charge]] =
chargeColl.ext.find($doc("userId" -> user.id)).sort($doc("date" -> -1)).list[Charge]()
private val topPatronUserIdsNb = 300
private val topPatronUserIdsCache = new PeriodicRefreshCache[List[User.ID]](
@ -283,16 +285,18 @@ final class PlanApi(
every = Every(1 hour),
atMost = AtMost(1 minute),
f = () => chargeColl.aggregateList(
Match($doc("userId" $exists true)), List(
maxDocs = topPatronUserIdsNb * 2,
readPreference = ReadPreference.secondaryPreferred
) { framework =>
import framework._
Match($doc("userId" $exists true)) -> List(
GroupField("userId")("total" -> SumField("cents")),
Sort(Descending("total")),
Limit(topPatronUserIdsNb * 3 / 2)
),
maxDocs = topPatronUserIdsNb * 2,
readPreference = ReadPreference.secondaryPreferred
).map {
_.flatMap { _.getAs[User.ID]("_id") }
} flatMap filterUserIds map (_ take topPatronUserIdsNb),
)
}.dmap {
_.flatMap { _.getAsOpt[User.ID]("_id") }
} flatMap filterUserIds dmap (_ take topPatronUserIdsNb),
default = Nil,
initialDelay = 35 seconds
)
@ -301,13 +305,13 @@ final class PlanApi(
private def filterUserIds(ids: List[User.ID]): Fu[List[User.ID]] = {
val dedup = ids.distinct
UserRepo.filterByEnabledPatrons(dedup) map { enableds =>
userRepo.filterByEnabledPatrons(dedup) map { enableds =>
dedup filter enableds.contains
}
}
private def addCharge(charge: Charge): Funit =
chargeColl.insert(charge).void >>- {
chargeColl.insert.one(charge).void >>- {
recentChargeUserIdsCache.refresh
monthlyGoalApi.get foreach { m =>
Bus.publish(lila.hub.actorApi.plan.ChargeEvent(
@ -364,7 +368,7 @@ final class PlanApi(
}
private def setDbUserPlan(user: User, plan: lila.user.Plan): Funit =
UserRepo.setPlan(user, plan) >>- lightUserApi.invalidate(user.id)
userRepo.setPlan(user, plan) >>- lightUserApi.invalidate(user.id)
private def createCustomer(user: User, data: Checkout, plan: StripePlan): Fu[StripeCustomer] =
stripeClient.createCustomer(user, data, plan) flatMap { customer =>
@ -375,12 +379,12 @@ final class PlanApi(
}
private def saveStripePatron(user: User, customerId: CustomerId, freq: Freq): Funit = userPatron(user) flatMap {
case None => patronColl.insert(Patron(
case None => patronColl.insert.one(Patron(
_id = Patron.UserId(user.id),
stripe = Patron.Stripe(customerId).some,
lastLevelUp = DateTime.now
).expireInOneMonth(!freq.renew))
case Some(patron) => patronColl.update(
case Some(patron) => patronColl.update.one(
$id(patron.id),
patron.copy(
stripe = Patron.Stripe(customerId).some

View File

@ -1,6 +1,6 @@
package lila.plan
import akka.actor.ActorSelection
import akka.actor._
import scala.concurrent.duration._
import lila.hub.actorApi.timeline.{ Propagate }
@ -10,12 +10,11 @@ import lila.user.User
private[plan] final class PlanNotifier(
notifyApi: NotifyApi,
scheduler: lila.common.Scheduler,
timeline: ActorSelection
) {
timeline: lila.hub.actors.Timeline
)(implicit system: ActorSystem) {
def onStart(user: User) = fuccess {
scheduler.once(5 seconds) {
system.scheduler.scheduleOnce(5 seconds) {
notifyApi.addNotification(Notification.make(
Notifies(user.id),
lila.notify.PlanStart(user.id)

View File

@ -1,12 +1,15 @@
package lila.plan
import play.api.libs.json._
import play.api.libs.ws.{ WS, WSResponse }
import play.api.Play.current
import play.api.libs.ws.{ WSClient, WSResponse }
import lila.common.config.Secret
import lila.user.User
private final class StripeClient(config: StripeClient.Config) {
private final class StripeClient(
ws: WSClient,
config: StripeClient.Config
) {
import StripeClient._
import JsonHandlers._
@ -125,7 +128,7 @@ private final class StripeClient(config: StripeClient.Config) {
private def post[A: Reads](url: String, data: Seq[(String, Any)]): Fu[A] = {
logger.info(s"POST $url ${debugInput(data)}")
request(url).post(fixInput(data).toMap mapValues { Seq(_) }) flatMap response[A]
request(url).post(fixInput(data).toMap) flatMap response[A]
}
private def delete[A: Reads](url: String, data: Seq[(String, Any)]): Fu[A] = {
@ -134,7 +137,7 @@ private final class StripeClient(config: StripeClient.Config) {
}
private def request(url: String) =
WS.url(s"${config.endpoint}/$url").withHeaders("Authorization" -> s"Bearer ${config.secretKey}")
ws.url(s"${config.endpoint}/$url").withHeaders("Authorization" -> s"Bearer ${config.secretKey}")
private def response[A: Reads](res: WSResponse): Fu[A] = res.status match {
case 200 => (implicitly[Reads[A]] reads res.json).fold(
@ -156,9 +159,9 @@ private final class StripeClient(config: StripeClient.Config) {
(js.asOpt[JsObject] flatMap { o => (o \ "deleted").asOpt[Boolean] }) == Some(true)
private def fixInput(in: Seq[(String, Any)]): Seq[(String, String)] = (in map {
case (sym, Some(x)) => Some(sym.name -> x.toString)
case (sym, None) => None
case (sym, x) => Some(sym.name -> x.toString)
case (name, Some(x)) => Some(name -> x.toString)
case (name, None) => None
case (name, x) => Some(name -> x.toString)
}).flatten
private def listReader[A: Reads]: Reads[List[A]] = (__ \ "data").read[List[A]]
@ -166,7 +169,7 @@ private final class StripeClient(config: StripeClient.Config) {
private def debugInput(data: Seq[(String, Any)]) = fixInput(data) map { case (k, v) => s"$k=$v" } mkString " "
}
object StripeClient {
private object StripeClient {
class StripeException(msg: String) extends Exception(msg)
class DeletedException(msg: String) extends StripeException(msg)
@ -174,5 +177,11 @@ object StripeClient {
class NotFoundException(msg: String) extends StatusException(msg)
class InvalidRequestException(msg: String) extends StatusException(msg)
case class Config(endpoint: String, publicKey: String, secretKey: String)
import io.methvin.play.autoconfig._
case class Config(
endpoint: String,
@ConfigName("keys.public") publicKey: String,
@ConfigName("keys.public") secretKey: Secret
)
implicit val configLoader = AutoConfig.loader[Config]
}

View File

@ -2,8 +2,6 @@ package lila.plan
import org.joda.time.DateTime
case class PayPalIpnKey(value: String) extends AnyVal
case class CustomerId(value: String) extends AnyVal
case class ChargeId(value: String) extends AnyVal

View File

@ -7,6 +7,7 @@ import play.api.Configuration
import scala.concurrent.duration._
import lila.common.config._
import lila.hub.actors
@Module
private class RelationConfig(
@ -20,7 +21,9 @@ private class RelationConfig(
final class Env(
appConfig: Configuration,
db: lila.db.Env,
hub: lila.hub.Env,
relation: actors.Relation,
timeline: actors.Timeline,
report: actors.Report,
onlineUserIds: () => Set[lila.user.User.ID],
lightUserApi: lila.user.LightUserApi,
followable: String => Fu[Boolean],
@ -38,9 +41,9 @@ final class Env(
lazy val api = new RelationApi(
coll = coll,
repo = repo,
actor = hub.relation,
timeline = hub.timeline,
reporter = hub.report,
actor = relation,
timeline = timeline,
reporter = report,
followable = followable,
asyncCache = asyncCache,
maxFollow = config.maxFollow,

View File

@ -11,15 +11,16 @@ import lila.common.Bus
import lila.common.config.Max
import lila.db.dsl._
import lila.db.paginator._
import lila.hub.actors
import lila.hub.actorApi.timeline.{ Propagate, Follow => FollowUser }
import lila.user.User
final class RelationApi(
coll: Coll,
repo: RelationRepo,
actor: ActorSelection,
timeline: ActorSelection,
reporter: ActorSelection,
actor: actors.Relation,
timeline: actors.Timeline,
reporter: actors.Report,
followable: ID => Fu[Boolean],
asyncCache: lila.memo.AsyncCache.Builder,
maxFollow: Max,