Show appropriate message in case of login failure #35

master
mgifos 7 years ago
parent 9fce3c19ac
commit d757b72cd3
  1. 4
      src/main/resources/application.conf
  2. 53
      src/main/scala/com.github.mgifos.workouts/GarminConnect.scala
  3. 18
      src/main/scala/com.github.mgifos.workouts/Main.scala

@ -0,0 +1,4 @@
akka {
stdout-loglevel = "OFF"
loglevel = "OFF"
}

@ -28,9 +28,9 @@ import scala.util.Failure
case class GarminWorkout(name: String, id: Long) case class GarminWorkout(name: String, id: Long)
class GarminConnect(email: String, password: String)(implicit system: ActorSystem, executionContext: ExecutionContext, mat: Materializer) { case class GarminSession(headers: Seq[HttpHeader])
case class Session(headers: Seq[HttpHeader]) class GarminConnect(email: String, password: String)(implicit system: ActorSystem, executionContext: ExecutionContext, mat: Materializer) {
case class Login(forceNewSession: Boolean) case class Login(forceNewSession: Boolean)
@ -42,11 +42,10 @@ class GarminConnect(email: String, password: String)(implicit system: ActorSyste
* @param workouts * @param workouts
* @return * @return
*/ */
def createWorkouts(workouts: Seq[WorkoutDef]): Future[Seq[GarminWorkout]] = { def createWorkouts(workouts: Seq[WorkoutDef])(implicit session: GarminSession): Future[Seq[GarminWorkout]] = {
val source = Source.fromFuture(login()).flatMapConcat { session =>
log.info("\nCreating workouts:") log.info("\nCreating workouts:")
Source(workouts.map { workout => val source = Source(workouts.map { workout =>
val req = Post("https://connect.garmin.com/modern/proxy/workout-service/workout") val req = Post("https://connect.garmin.com/modern/proxy/workout-service/workout")
.withEntity(HttpEntity(`application/json`, workout.json.toString())) .withEntity(HttpEntity(`application/json`, workout.json.toString()))
.withHeaders(session.headers .withHeaders(session.headers
@ -54,8 +53,6 @@ class GarminConnect(email: String, password: String)(implicit system: ActorSyste
:+ RawHeader("NK", "NT")) :+ RawHeader("NK", "NT"))
workout.name -> req workout.name -> req
}) })
}
val flow = Flow[(String, HttpRequest)] val flow = Flow[(String, HttpRequest)]
.throttle(1, 1.second, 1, ThrottleMode.shaping) .throttle(1, 1.second, 1, ThrottleMode.shaping)
.mapAsync(1) { .mapAsync(1) {
@ -72,7 +69,6 @@ class GarminConnect(email: String, password: String)(implicit system: ActorSyste
} }
} }
} }
source.via(flow).runWith(Sink.seq) source.via(flow).runWith(Sink.seq)
} }
@ -82,16 +78,13 @@ class GarminConnect(email: String, password: String)(implicit system: ActorSyste
* @param workouts Workout names * @param workouts Workout names
* @return Count of deleted items * @return Count of deleted items
*/ */
def deleteWorkouts(workouts: Seq[String]): Future[Int] = { def deleteWorkouts(workouts: Seq[String])(implicit session: GarminSession): Future[Int] = {
val futureRequests = for { val futureRequests = getWorkoutsMap().map { wsMap =>
session <- login()
map <- getWorkoutsMap()
} yield {
log.info("\nDeleting workouts:") log.info("\nDeleting workouts:")
for { for {
workout <- workouts workout <- workouts
ids = map.getOrElse(workout, Seq.empty) ids = wsMap.getOrElse(workout, Seq.empty)
if ids.nonEmpty if ids.nonEmpty
} yield { } yield {
val label = s"$workout -> ${ids.mkString("[", ", ", "]")}" val label = s"$workout -> ${ids.mkString("[", ", ", "]")}"
@ -115,9 +108,8 @@ class GarminConnect(email: String, password: String)(implicit system: ActorSyste
source.runWith(Sink.seq).map(_.length) source.runWith(Sink.seq).map(_.length)
} }
def schedule(spec: Seq[(LocalDate, GarminWorkout)]): Future[Int] = { def schedule(spec: Seq[(LocalDate, GarminWorkout)])(implicit session: GarminSession): Future[Int] = {
log.debug(s" Scheduling spec: ${spec.mkString("\n")}") log.debug(s" Scheduling spec: ${spec.mkString("\n")}")
Source.fromFuture(login(forceNewSession = true)).flatMapConcat { session =>
log.info("\nScheduling:") log.info("\nScheduling:")
Source(spec).map { Source(spec).map {
case (date, gw) => case (date, gw) =>
@ -135,20 +127,20 @@ class GarminConnect(email: String, password: String)(implicit system: ActorSyste
else log.error(s" Cannot schedule: $label") else log.error(s" Cannot schedule: $label")
} }
} }
}.runWith(Sink.seq).map(_.length) .runWith(Sink.seq).map(_.length)
} }
/** /**
* Retrieves workout mapping: name -> Seq[id] @ GarminConnect * Retrieves workout mapping: name -> Seq[id] @ GarminConnect
* @return * @return
*/ */
private def getWorkoutsMap(): Future[Map[String, Seq[Long]]] = { private def getWorkoutsMap()(implicit session: GarminSession): Future[Map[String, Seq[Long]]] = {
val source = Source.fromFuture(login()).flatMapConcat { session =>
val req = Get("https://connect.garmin.com/modern/proxy/workout-service/workouts?start=1&limit=9999&myWorkoutsOnly=true&sharedWorkoutsOnly=false") val req = Get("https://connect.garmin.com/modern/proxy/workout-service/workouts?start=1&limit=9999&myWorkoutsOnly=true&sharedWorkoutsOnly=false")
.withHeaders(session.headers .withHeaders(session.headers
:+ Referer("https://connect.garmin.com/modern/workouts") :+ Referer("https://connect.garmin.com/modern/workouts")
:+ RawHeader("NK", "NT")) :+ RawHeader("NK", "NT"))
Source.fromFuture( val source = Source.fromFuture(
Http().singleRequest(req).flatMap { res => Http().singleRequest(req).flatMap { res =>
if (res.status == OK) if (res.status == OK)
res.body.map { json => res.body.map { json =>
@ -163,22 +155,22 @@ class GarminConnect(email: String, password: String)(implicit system: ActorSyste
Future.failed(new Error("Cannot retrieve workout list from Garmin Connect")) Future.failed(new Error("Cannot retrieve workout list from Garmin Connect"))
} }
}) })
}
source.runWith(Sink.head) source.runWith(Sink.head)
} }
private lazy val loginActor: ActorRef = system.actorOf(Props(new LoginActor())) private lazy val loginActor: ActorRef = system.actorOf(Props(new LoginActor()))
private def login(forceNewSession: Boolean = false): Future[Session] = ask(loginActor, Login(forceNewSession))(Timeout(2.minutes)).mapTo[Session] def login(forceNewSession: Boolean = false): Future[Either[String, GarminSession]] =
ask(loginActor, Login(forceNewSession))(Timeout(2.minutes)).mapTo[Either[String, GarminSession]]
/** /**
* Holds and reloads session if neccessary * Holds and reloads session if neccessary
*/ */
class LoginActor extends Actor { class LoginActor extends Actor {
private case class NewSession(session: Session) private case class NewSession(session: GarminSession)
var maybeSession: Option[Session] = None var maybeSession: Option[GarminSession] = None
override def receive = { override def receive = {
@ -189,19 +181,22 @@ class GarminConnect(email: String, password: String)(implicit system: ActorSyste
case _ => case _ =>
login.andThen { login.andThen {
case util.Success(x) => case util.Success(x) =>
origin ! x if (x.headers.exists(_.value().matches("""SESSIONID=[a-z\d-]{5,}"""))) {
origin ! Right(x)
self ! NewSession(x) self ! NewSession(x)
} else
origin ! Left("Login was not successful, check your username and password and try again.")
case Failure(_) => case Failure(_) =>
log.error("Failed to log in to Garmin Connect") origin ! Left("Attempt to log in to Garmin Connect was not successful (this could be a server error).")
} }
} }
case NewSession(session) => case NewSession(session) =>
if (maybeSession.isEmpty) log.info("Successfully logged in to Garmin Connect") if (maybeSession.isEmpty) log.info("Successfully logged in to Garmin Connect!")
maybeSession = Option(session) maybeSession = Option(session)
} }
private def login: Future[Session] = { private def login: Future[GarminSession] = {
def extractCookies(res: HttpResponse) = res.headers.collect { case x: `Set-Cookie` => x.cookie }.map(c => Cookie(c.name, c.value)) def extractCookies(res: HttpResponse) = res.headers.collect { case x: `Set-Cookie` => x.cookie }.map(c => Cookie(c.name, c.value))
@ -265,7 +260,7 @@ class GarminConnect(email: String, password: String)(implicit system: ActorSyste
"password" -> password, "password" -> password,
"embed" -> "false")).toEntity).withHeaders(extractCookies(res1))).withoutBody "embed" -> "false")).toEntity).withHeaders(extractCookies(res1))).withoutBody
sessionCookies <- redirectionLoop(0, "https://connect.garmin.com/modern", extractCookies(res2)) sessionCookies <- redirectionLoop(0, "https://connect.garmin.com/modern", extractCookies(res2))
} yield Session(sessionCookies) } yield GarminSession(sessionCookies)
} }
} }
} }

@ -6,8 +6,7 @@ import java.time.LocalDate
import akka.actor.ActorSystem import akka.actor.ActorSystem
import akka.http.scaladsl.Http import akka.http.scaladsl.Http
import akka.stream.ActorMaterializer import akka.stream.ActorMaterializer
import com.github.mgifos.workouts.model._ import com.github.mgifos.workouts.model.{ WeeklyPlan, _ }
import com.github.mgifos.workouts.model.WeeklyPlan
import com.typesafe.scalalogging.Logger import com.typesafe.scalalogging.Logger
import scopt.OptionParser import scopt.OptionParser
@ -46,7 +45,7 @@ object Main extends App {
val worker = run(config).andThen { val worker = run(config).andThen {
case _ => case _ =>
shutdown() shutdown()
log.info("Logged out and closed connection") log.info("Logged out. Connection is closed.")
} }
Await.result(worker, 10.minutes) Await.result(worker, 10.minutes)
log.info("Bye") log.info("Bye")
@ -123,6 +122,9 @@ object Main extends App {
val workouts = plan.workouts.toIndexedSeq val workouts = plan.workouts.toIndexedSeq
garmin.login().flatMap {
case Right(s) =>
implicit val session: GarminSession = s
for { for {
maybeDeleteMessage <- deleteWorkoutsTask(workouts.map(_.name)) maybeDeleteMessage <- deleteWorkoutsTask(workouts.map(_.name))
maybeGarminWorkouts <- createWorkoutsTask(workouts) maybeGarminWorkouts <- createWorkoutsTask(workouts)
@ -133,26 +135,30 @@ object Main extends App {
maybeGarminWorkouts.foreach(workouts => log.info(s" ${workouts.length} imported")) maybeGarminWorkouts.foreach(workouts => log.info(s" ${workouts.length} imported"))
maybeScheduleMessage.foreach(msg => log.info(" " + msg)) maybeScheduleMessage.foreach(msg => log.info(" " + msg))
} }
case Left(loginFailureMessage) =>
log.error(loginFailureMessage)
Future.successful(())
}
} }
/** /**
* Deletes existing workouts with the same names or not * Deletes existing workouts with the same names or not
*/ */
private def deleteWorkoutsTask(workouts: Seq[String])(implicit config: Config, garmin: GarminConnect): Future[Option[String]] = { private def deleteWorkoutsTask(workouts: Seq[String])(implicit config: Config, garmin: GarminConnect, session: GarminSession): Future[Option[String]] = {
if (config.delete) if (config.delete)
garmin.deleteWorkouts(workouts).map(c => Some(s"$c deleted")) garmin.deleteWorkouts(workouts).map(c => Some(s"$c deleted"))
else else
Future.successful(None) Future.successful(None)
} }
private def createWorkoutsTask(workouts: Seq[WorkoutDef])(implicit config: Config, garmin: GarminConnect): Future[Option[Seq[GarminWorkout]]] = { private def createWorkoutsTask(workouts: Seq[WorkoutDef])(implicit config: Config, garmin: GarminConnect, session: GarminSession): Future[Option[Seq[GarminWorkout]]] = {
if (config.mode.exists(Seq(Modes.`import`, Modes.schedule).contains)) if (config.mode.exists(Seq(Modes.`import`, Modes.schedule).contains))
garmin.createWorkouts(workouts).map(Option.apply) garmin.createWorkouts(workouts).map(Option.apply)
else else
Future.successful(None) Future.successful(None)
} }
private def scheduleTask(workouts: Seq[GarminWorkout])(implicit config: Config, garmin: GarminConnect, plan: WeeklyPlan): Future[Option[String]] = { private def scheduleTask(workouts: Seq[GarminWorkout])(implicit config: Config, garmin: GarminConnect, plan: WeeklyPlan, session: GarminSession): Future[Option[String]] = {
if (config.mode.contains(Modes.schedule)) { if (config.mode.contains(Modes.schedule)) {

Loading…
Cancel
Save