Skip to content

Commit

Permalink
Upgrade to citywasp-api 2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
2m committed Aug 10, 2021
1 parent e240d7d commit fe9da51
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 123 deletions.
8 changes: 8 additions & 0 deletions .scalafix.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
rule = SortImports
SortImports.blocks = [
"java.",
"scala.",
"akka.",
"*",
"lt.dvim.citywasp.",
]
13 changes: 10 additions & 3 deletions .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
version = "2.7.5"

align = some
version = "3.0.0-RC6"
align = more
maxColumn = 120

align.tokens."+" = [
{code = "%", owner = "Term.ApplyInfix"},
{code = "%%", owner = "Term.ApplyInfix"},
]

rewrite.rules = [RedundantBraces, RedundantParens, SortImports]
unindentTopLevelOperators = true
33 changes: 0 additions & 33 deletions .travis.yml

This file was deleted.

21 changes: 12 additions & 9 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@ micrositePalette := Map(
"white-color" -> "#FFFFFF"
)

resolvers += Resolver.bintrayRepo("2m", "maven")
scalaVersion := "2.13.6"
libraryDependencies ++= Seq(
"citywasp" %% "citywasp-api" % "1.2",
"com.typesafe.akka" %% "akka-stream" % "2.6.14",
"com.typesafe.akka" %% "akka-http" % "10.2.4",
"de.heikoseeberger" %% "akka-http-circe" % "1.36.0",
"com.danielasfregola" %% "twitter4s" % "7.0",
"org.scala-lang.modules" %% "scala-java8-compat" % "0.9.1"
"lt.dvim.citywasp" %% "citywasp-api" % "2.0.2",
"com.typesafe.akka" %% "akka-stream" % "2.6.15",
"com.typesafe.akka" %% "akka-http" % "10.2.6",
"de.heikoseeberger" %% "akka-http-circe" % "1.37.0",
"com.softwaremill.sttp.client3" %% "akka-http-backend" % "3.3.13",
"com.danielasfregola" %% "twitter4s" % "7.0",
"org.scala-lang.modules" %% "scala-java8-compat" % "0.9.0"
)

licenses += ("Apache-2.0", url("http://www.apache.org/licenses/LICENSE-2.0"))
Expand All @@ -42,10 +43,12 @@ developers += Developer(
"https://gitter.im/2m/kabrioletas",
url("https://github.com/2m/kabrioletas/graphs/contributors")
)
bintrayOrganization := Some("2m")
bintrayRepository := (if (isSnapshot.value) "snapshots" else "maven")
organizationName := "https://github.com/2m/kabrioletas/graphs/contributors"

scalafmtOnCompile := true
scalafixOnCompile := true
ThisBuild / scalafixDependencies ++= Seq(
"com.nequissimus" %% "sort-imports" % "0.5.5"
)

enablePlugins(AutomateHeaderPlugin, JavaAppPackaging, MicrositesPlugin)
2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.4.7
sbt.version=1.5.5
14 changes: 7 additions & 7 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
addSbtPlugin("com.dwijnand" % "sbt-dynver" % "4.1.1")
addSbtPlugin("com.dwijnand" % "sbt-travisci" % "1.2.0")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.2")
addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.6.0")
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.8.1")
addSbtPlugin("com.47deg" % "sbt-microsites" % "1.3.4")
addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.6.1")
addSbtPlugin("com.dwijnand" % "sbt-dynver" % "4.1.1")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.2")
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.29")
addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.6.0")
addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.1.20")
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.8.1")
addSbtPlugin("com.47deg" % "sbt-microsites" % "1.3.4")
8 changes: 5 additions & 3 deletions src/main/resources/application.conf
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
include "reference"

dry-run = yes

citywasp {
email = ""
password = ""
app-version = "9.9.9"
uris = []
}

opencagedata.key = ""

kabrioletas {
car-id = 719
car-id = 96
poll-interval = 1 minute
}

Expand Down
125 changes: 82 additions & 43 deletions src/main/scala/Kabrioletas.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,39 @@ package lt.dvim.citywasp.kabrioletas

import java.time.{Duration, Instant}

import scala.compat.java8.DurationConverters._
import scala.concurrent.Future
import scala.concurrent.duration.{Duration => _, _}
import scala.jdk.CollectionConverters._
import scala.util.{Failure, Random}

import akka.actor.SupervisorStrategy.Resume
import akka.actor.{Actor, ActorLogging, ActorSystem, OneForOneStrategy, Props}
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.Uri.Query
import akka.http.scaladsl.model.{HttpRequest, Uri}
import akka.http.scaladsl.unmarshalling.Unmarshal
import akka.pattern.pipe
import akka.stream.ActorMaterializer
import citywasp.api._

import cats.implicits._
import com.danielasfregola.twitter4s.TwitterRestClient
import com.danielasfregola.twitter4s.entities.{RatedData, Tweet}
import com.danielasfregola.twitter4s.entities.Tweet
import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport
import eu.timepit.refined._
import eu.timepit.refined.auto._
import io.circe._
import sttp.client3.akkahttp.AkkaHttpBackend
import sttp.model.{Uri => SttpUri}
import sttp.tapir.client.sttp.SttpClientInterpreter

import scala.compat.java8.DurationConverters._
import scala.concurrent.duration.{Duration => _, _}
import scala.util.{Failure, Random}
import lt.dvim.citywasp.api.Model._
import lt.dvim.citywasp.api._
import lt.dvim.citywasp.kabrioletas.ResponseOps._

object CabrioCheck {
case object DoTheCheck
case class ParkedCars(cars: Seq[Car])
case object Tweeted
case class ParkedCars(cars: List[Car])
case class LastTweetAndCar(tweets: List[Tweet], car: Option[Car])
case class CarWithLocation(car: Car, location: OpenCageData.Location)
}
Expand All @@ -48,52 +60,61 @@ class CabrioCheck extends Actor with ActorLogging {
import context.dispatcher

implicit val sys = context.system
implicit val mat = ActorMaterializer()

val config = context.system.settings.config.getConfig("citywasp")
implicit val cw = RemoteCityWasp(config)
val backend = AkkaHttpBackend.usingActorSystem(sys)

val twitter = TwitterRestClient()

final val DryRun = context.system.settings.config.getBoolean("dry-run")
final val CitywaspAppVersion = {
val str = context.system.settings.config.getString("citywasp.app-version")
val refined: Either[String, AppVersion] = refineV(str)
refined.getOrElse(throw new Exception(s"unable to parse [$str] to a version"))
}
final val CitywaspUris =
context.system.settings.config.getStringList("citywasp.uris").asScala.toList.map(SttpUri.unsafeParse)
final val OpenCageDataKey = context.system.settings.config.getString("opencagedata.key")
final val CardIdToSearch = context.system.settings.config.getInt("kabrioletas.car-id")
final val ServiceIdToSearch = context.system.settings.config.getInt("kabrioletas.service-id")
final val PollInterval = context.system.settings.config.getDuration("kabrioletas.poll-interval").toScala

var lastTweetAt: Instant = _

context.system.scheduler.schedule(0.seconds, PollInterval, self, DoTheCheck)
context.system.scheduler.scheduleAtFixedRate(0.seconds, PollInterval, self, DoTheCheck)

override def preStart() = {
override def preStart() =
resetLastTweetTimer()
}

def receive = {
case DoTheCheck =>
log.info("Starting to look for a wanted car.")
cw.login.pipeTo(self)
case login: LoggedIn =>
log.info("Successfully logged in!")
login.availableCars.map(ParkedCars).pipeTo(self)
allCars().pipeTo(self)
()
case ParkedCars(cars) =>
val car = cars.find(_.id == CardIdToSearch)
log.info(s"Currently total ${cars.size} cars available.")
val car = cars.find(_.serviceId == ServiceIdToSearch)
log.info(s"Car search resulted in $car")
twitter.homeTimeline(count = 1).map(timeline => LastTweetAndCar(timeline.data.toList, car)).pipeTo(self)
()
case LastTweetAndCar(Nil, None) =>
log.info(s"No tweets and no car. Keep on searching...")
if (Duration.between(Instant.now, lastTweetAt).toDays.abs >= 1) {
tweetAboutSearch().pipeTo(self)
}
()
case LastTweetAndCar(Nil, Some(car)) =>
log.info(s"Found a car. It is gonna be a great first tweet!")
reverseGeocodeCarLocation(car).map(CarWithLocation(car, _)).pipeTo(self)
()
case LastTweetAndCar(tweet :: _, Some(car)) =>
if (!tweet.text.contains("ready")) {
log.info(
s"Found a car [$car] and last tweet was about taken car [${tweet.text}]. Let's tell the world about the car we just found!"
)
reverseGeocodeCarLocation(car).map(CarWithLocation(car, _)).pipeTo(self)
} else if (
tweet.coordinates.isDefined && math.abs(tweet.coordinates.get.coordinates.head - car.lon) > 0.001 && math
.abs(tweet.coordinates.get.coordinates.tail.head - car.lat) > 0.001
tweet.coordinates.isDefined && math.abs(
tweet.coordinates.get.coordinates.head - car.long.toDouble
) > 0.001 && math
.abs(tweet.coordinates.get.coordinates.tail.head - car.lat.toDouble) > 0.001
) {
log.info(
s"Found a car [$car] and last tweet was about a parked car [${tweet.text}], but it was on a different place. Let tell the world about the car we just found!"
Expand All @@ -102,6 +123,7 @@ class CabrioCheck extends Actor with ActorLogging {
} else {
log.info(s"Found a car [$car] and last tweet was about a parked car [${tweet.text}] at the same place.")
}
()
case LastTweetAndCar(tweet :: _, None) =>
if (tweet.text.contains("ready")) {
log.info(s"No car and last tweet was about a parked car [${tweet.text}]. Tweet about taken car.")
Expand All @@ -112,10 +134,12 @@ class CabrioCheck extends Actor with ActorLogging {
tweetAboutSearch().pipeTo(self)
}
}
case cwl @ CarWithLocation(car, location) =>
()
case cwl @ CarWithLocation(_, location) =>
log.info(s"Reverse geocoded car location to $location")
tweetAbout(cwl).pipeTo(self)
case t: Tweet =>
()
case Tweeted =>
log.info("Tweet success.")
resetLastTweetTimer()
case m =>
Expand All @@ -127,17 +151,27 @@ class CabrioCheck extends Actor with ActorLogging {
case akka.actor.Status.Failure(ex) =>
log.error(ex.getMessage)
ex.printStackTrace()
case _ => log.error(m.toString)
}
}

def allCars() =
CitywaspUris.map(cars(CitywaspAppVersion)).combineAll.map(ParkedCars.apply)

def cars(appVersion: AppVersion)(uri: SttpUri) = {
val params = Params.default.copy(appVersion = appVersion, country = Country.fromUri(uri))
val carsRequest = SttpClientInterpreter().toRequest(Api.CarsLive.GetAvailableCars, Some(uri)).apply(params)
carsRequest.send(backend).flatMap(_.toFuture)
}

def reverseGeocodeCarLocation(car: Car)(implicit sys: ActorSystem) = {
import OpenCageData._

Http()
.singleRequest(
HttpRequest(
uri = Uri("http://api.opencagedata.com/geocode/v1/json")
.withQuery(Query("q" -> s"${car.lat},${car.lon}", "key" -> OpenCageDataKey))
.withQuery(Query("q" -> s"${car.lat},${car.long}", "key" -> OpenCageDataKey))
)
)
.flatMap(resp => Unmarshal(resp.entity).to[Location])
Expand All @@ -152,29 +186,33 @@ class CabrioCheck extends Actor with ActorLogging {
} yield s"$suburb$city"

val locationDescription = cityDescription.orElse(location.town).map(location => s" in $location").getOrElse("")

twitter.createTweet(
status =
f"\uD83D\uDE95\uD83D\uDE95\uD83D\uDE95 Parked and ready for a new adventure$locationDescription. Pick me up! https://www.google.com/maps?q=${car.lat}%.6f,${car.lon}%.6f ($randomMarker)",
latitude = Some(car.lat.toLong),
longitude = Some(car.lon.toLong),
display_coordinates = true
)
val tweet =
f"\uD83D\uDE95\uD83D\uDE95\uD83D\uDE95 Parked and ready for a new adventure$locationDescription. Pick me up! https://www.google.com/maps?q=${car.lat}%.6f,${car.long}%.6f ($randomMarker)"

if (DryRun) {
log.info(s"Would tweet: $tweet")
} else {
twitter.createTweet(
status = tweet,
latitude = Some(car.lat.toLong),
longitude = Some(car.long.toLong),
display_coordinates = true
)
}
Future.successful(Tweeted)
}

def tweetAboutNoCar() = {
def tweetAboutNoCar() =
twitter.createTweet(
status =
s"\uD83D\uDD1C\uD83D\uDD1C\uD83D\uDD1C I am on a ride right now. Will let you know when I am free! ($randomMarker)"
)
}

def tweetAboutSearch() = {
def tweetAboutSearch() =
twitter.createTweet(
status =
s"🔎🔎🔎 There has been no available car for quite some time now. Nevertheless, I keep on searching. Stay tuned! ($randomMarker)"
)
}

def randomMarker =
Random.alphanumeric.take(6).mkString
Expand All @@ -185,12 +223,13 @@ class CabrioCheck extends Actor with ActorLogging {

class Supervisor extends Actor {
override def supervisorStrategy =
OneForOneStrategy(loggingEnabled = true) { case t =>
OneForOneStrategy(loggingEnabled = true) { case _ =>
Resume
}

override def preStart() = {
context.actorOf(Props[CabrioCheck], "cabrioCheck")
context.actorOf(Props[CabrioCheck](), "cabrioCheck")
()
}

def receive = { case _ =>
Expand All @@ -199,7 +238,7 @@ class Supervisor extends Actor {

object Kabrioletas extends App {
val sys = ActorSystem("Kabrioletas")
sys.actorOf(Props[Supervisor], "supervisor")
sys.actorOf(Props[Supervisor](), "supervisor")
}

object OpenCageData extends FailFastCirceSupport {
Expand All @@ -208,9 +247,9 @@ object OpenCageData extends FailFastCirceSupport {
implicit val decodeLocation: Decoder[Location] = new Decoder[Location] {
final def apply(c: HCursor): Decoder.Result[Location] =
for {
suburb <- c.downField("results").downArray.first.downField("components").downField("suburb").as[Option[String]]
city <- c.downField("results").downArray.first.downField("components").downField("city").as[Option[String]]
town <- c.downField("results").downArray.first.downField("components").downField("town").as[Option[String]]
suburb <- c.downField("results").downArray.downField("components").downField("suburb").as[Option[String]]
city <- c.downField("results").downArray.downField("components").downField("city").as[Option[String]]
town <- c.downField("results").downArray.downField("components").downField("town").as[Option[String]]
} yield Location(suburb, city, town)
}
}
Loading

0 comments on commit fe9da51

Please sign in to comment.