Initial commit

This commit is contained in:
Eugene Zadyra
2018-05-31 00:28:58 +02:00
commit 68557d960a
116 changed files with 8590 additions and 0 deletions

122
.gitignore vendored Normal file
View File

@@ -0,0 +1,122 @@
# Created by .ignore support plugin (hsz.mobi)
### Java template
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
### macOS template
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Scala template
*.class
*.log
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# CMake
cmake-build-debug/
cmake-build-release/
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
### Gradle template
.gradle
/build/
# Ignore Gradle GUI config
gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
# Cache of project
.gradletasknamecache
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
# gradle/wrapper/gradle-wrapper.properties
/.idea/
/**/*.iml

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) ${year} ${name}
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

22
README.md Normal file
View File

@@ -0,0 +1,22 @@
# Luxmed Booking Service
Non official bot for **Portal Pacienta LUX MED**.
With its help user can book a visit to a doctor, create term monitoring, view upcoming visits and visit history.
It is available by [@luxmedbot](https://telegram.me/luxmedbot)
####To setup your own
1. create your own telegram bot using [@BotFather](https://telegram.me/botfather)
2. add to .bash_profile
```
export TELEGRAM_TOKEN="SOME TOKEN"
export SECURITY_SECRET="SOME SECRET FOR ENCODING USER PASSWORDS"
```
3. install postgres and create db **lbs** with login **lbs** and password **lsb123**
4. run using `./gradlew bootRun`
5. send `/start` to your bot

6
api/build.gradle Normal file
View File

@@ -0,0 +1,6 @@
dependencies {
compile project(':common')
compile group: "org.scalaj", name: "scalaj-http_2.12", version: "2.3.0"
compile group: "org.json4s", name: "json4s-jackson_2.12", version: "3.6.0-M3"
}

View File

@@ -0,0 +1,44 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api
import com.lbs.api.http.headers._
import scalaj.http.{Http, HttpRequest}
trait ApiBase {
private val CommonHeaders =
Map(
Host -> "portalpacjenta.luxmed.pl",
Accept -> "*/*",
Connection -> "keep-alive",
`Accept-Encoding` -> "gzip;q=1.0, compress;q=0.5",
`User-Agent` -> "PatientPortal/3.3.0 (pl.luxmed.pp.LUX-MED; build:166; iOS 11.3.0) Alamofire/4.5.1",
`Accept-Language` -> "en-PL;q=1.0, ru-PL;q=0.9, pl-PL;q=0.8, uk-PL;q=0.7"
)
protected def http(url: String): HttpRequest = {
Http(s"https://portalpacjenta.luxmed.pl/PatientPortalMobileAPI/api/$url").headers(CommonHeaders)
}
}

View File

@@ -0,0 +1,167 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import com.lbs.api.http._
import com.lbs.api.http.headers._
import com.lbs.api.json.JsonSerializer.extensions._
import com.lbs.api.json.model._
import scalaj.http.{HttpRequest, HttpResponse}
import com.lbs.api.ApiResponseMutators._
object LuxmedApi extends ApiBase {
private val dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
def login(username: String, password: String, clientId: String = "iPhone"): Either[Throwable, LoginResponse] = {
val request = http("token").
header(`Content-Type`, "application/x-www-form-urlencoded").
header(`x-api-client-identifier`, clientId).
param("client_id", clientId).
param("grant_type", "password").
param("password", password).
param("username", username)
post[LoginResponse](request)
}
def refreshToken(refreshToken: String, clientId: String = "iPhone"): Either[Throwable, LoginResponse] = {
val request = http("token").
header(`Content-Type`, "application/x-www-form-urlencoded").
header(`x-api-client-identifier`, clientId).
param("client_id", clientId).
param("grant_type", "refresh_token").
param("refresh_token", refreshToken)
post[LoginResponse](request)
}
def reservedVisits(accessToken: String, tokenType: String, fromDate: ZonedDateTime = ZonedDateTime.now(),
toDate: ZonedDateTime = ZonedDateTime.now().plusMonths(3)): Either[Throwable, ReservedVisitsResponse] = {
val request = http("visits/reserved").
header(`Content-Type`, "application/json").
header(Authorization, s"$tokenType $accessToken").
param("fromDate", dateFormat.format(fromDate)).
param("toDate", dateFormat.format(toDate))
get[ReservedVisitsResponse](request).mutate
}
def visitsHistory(accessToken: String, tokenType: String, fromDate: ZonedDateTime = ZonedDateTime.now().minusYears(1),
toDate: ZonedDateTime = ZonedDateTime.now(), page: Int = 1, pageSize: Int = 100): Either[Throwable, VisitsHistoryResponse] = {
val request = http("visits/history").
header(`Content-Type`, "application/json").
header(Authorization, s"$tokenType $accessToken").
param("fromDate", dateFormat.format(fromDate)).
param("toDate", dateFormat.format(toDate)).
param("page", page.toString).
param("pageSize", pageSize.toString)
get[VisitsHistoryResponse](request).mutate
}
def reservationFilter(accessToken: String, tokenType: String, fromDate: ZonedDateTime = ZonedDateTime.now(),
toDate: Option[ZonedDateTime] = None, cityId: Option[Long] = None, clinicId: Option[Long] = None,
serviceId: Option[Long] = None): Either[Throwable, ReservationFilterResponse] = {
val request = http("visits/available-terms/reservation-filter").
header(`Content-Type`, "application/json").
header(Authorization, s"$tokenType $accessToken").
param("cityId", cityId.map(_.toString)).
param("clinicId", clinicId.map(_.toString)).
param("fromDate", dateFormat.format(fromDate)).
param("toDate", toDate.map(dateFormat.format)).
param("serviceId", serviceId.map(_.toString))
get[ReservationFilterResponse](request).mutate
}
def availableTerms(accessToken: String, tokenType: String, payerId: Long, cityId: Long, clinicId: Option[Long], serviceId: Long, doctorId: Option[Long],
fromDate: ZonedDateTime = ZonedDateTime.now(), toDate: Option[ZonedDateTime] = None, timeOfDay: Int = 0,
languageId: Long = 10, findFirstFreeTerm: Boolean = false): Either[Throwable, AvailableTermsResponse] = {
val request = http("visits/available-terms").
header(`Content-Type`, "application/json").
header(Authorization, s"$tokenType $accessToken").
param("cityId", cityId.toString).
param("doctorId", doctorId.map(_.toString)).
param("findFirstFreeTerm", findFirstFreeTerm.toString).
param("fromDate", dateFormat.format(fromDate)).
param("languageId", languageId.toString).
param("payerId", payerId.toString).
param("clinicId", clinicId.map(_.toString)).
param("serviceId", serviceId.toString).
param("timeOfDay", timeOfDay.toString).
param("toDate", dateFormat.format(toDate.getOrElse(fromDate.plusMonths(3))))
get[AvailableTermsResponse](request).mutate
}
def temporaryReservation(accessToken: String, tokenType: String, temporaryReservationRequest: TemporaryReservationRequest): Either[Throwable, TemporaryReservationResponse] = {
val request = http("visits/temporary-reservation").
header(`Content-Type`, "application/json").
header(Authorization, s"$tokenType $accessToken")
post[TemporaryReservationResponse](request, bodyOpt = Some(temporaryReservationRequest))
}
def deleteTemporaryReservation(accessToken: String, tokenType: String, temporaryReservationId: Long): Either[Throwable, HttpResponse[String]] = {
val request = http(s"visits/temporary-reservation/$temporaryReservationId").
header(`Content-Type`, "application/json").
header(Authorization, s"$tokenType $accessToken")
delete(request)
}
def valuations(accessToken: String, tokenType: String, valuationsRequest: ValuationsRequest): Either[Throwable, ValuationsResponse] = {
val request = http("visits/available-terms/valuations").
header(`Content-Type`, "application/json").
header(Authorization, s"$tokenType $accessToken")
post[ValuationsResponse](request, bodyOpt = Some(valuationsRequest))
}
def reservation(accessToken: String, tokenType: String, reservationRequest: ReservationRequest): Either[Throwable, ReservationResponse] = {
val request = http("visits/reserved").
header(`Content-Type`, "application/json").
header(Authorization, s"$tokenType $accessToken")
post[ReservationResponse](request, bodyOpt = Some(reservationRequest))
}
def deleteReservation(accessToken: String, tokenType: String, reservationId: Long): Either[Throwable, HttpResponse[String]] = {
val request = http(s"visits/reserved/$reservationId").
header(`Content-Type`, "application/json").
header(Authorization, s"$tokenType $accessToken")
delete(request)
}
private def get[T <: SerializableJsonObject](request: HttpRequest)(implicit mf: scala.reflect.Manifest[T]): Either[Throwable, T] = {
request.toEither.map(_.body.as[T])
}
private def post[T <: SerializableJsonObject](request: HttpRequest, bodyOpt: Option[SerializableJsonObject] = None)(implicit mf: scala.reflect.Manifest[T]): Either[Throwable, T] = {
val postRequest = bodyOpt match {
case Some(body) => request.postData(body.asJson)
case None => request.postForm
}
postRequest.toEither.map(_.body.as[T])
}
private def delete(request: HttpRequest): Either[Throwable, HttpResponse[String]] = {
request.postForm.method("DELETE").toEither
}
}

View File

@@ -0,0 +1,95 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api
import java.time.ZonedDateTime
import com.lbs.api.json.model._
import scalaj.http.HttpResponse
import scala.concurrent.{ExecutionContext, Future}
import scala.language.implicitConversions
object LuxmedApiAsync {
private val syncApi = LuxmedApi
def login(username: String, password: String, clientId: String = "iPhone")(implicit ec: ExecutionContext): Future[LoginResponse] = {
async(syncApi.login(username, password, clientId))
}
def refreshToken(refreshToken: String, clientId: String = "iPhone")(implicit ec: ExecutionContext): Future[LoginResponse] = {
async(syncApi.refreshToken(refreshToken, clientId))
}
def reservedVisits(accessToken: String, tokenType: String, fromDate: ZonedDateTime = ZonedDateTime.now(),
toDate: ZonedDateTime = ZonedDateTime.now().plusMonths(3))(implicit ec: ExecutionContext): Future[ReservedVisitsResponse] = {
async(syncApi.reservedVisits(accessToken, tokenType, fromDate, toDate))
}
def visitsHistory(accessToken: String, tokenType: String, fromDate: ZonedDateTime = ZonedDateTime.now().minusYears(1),
toDate: ZonedDateTime, page: Int = 1, pageSize: Int = 100)(implicit ec: ExecutionContext): Future[VisitsHistoryResponse] = {
async(syncApi.visitsHistory(accessToken, tokenType, fromDate, toDate, page, pageSize))
}
def reservationFilter(accessToken: String, tokenType: String, fromDate: ZonedDateTime = ZonedDateTime.now(),
toDate: Option[ZonedDateTime] = None, cityId: Option[Long] = None,
serviceId: Option[Long] = None)(implicit ec: ExecutionContext): Future[ReservationFilterResponse] = {
async(syncApi.reservationFilter(accessToken, tokenType, fromDate, toDate, cityId, serviceId))
}
def availableTerms(accessToken: String, tokenType: String, payerId: Long, cityId: Long, clinicId: Option[Long], serviceId: Long, doctorId: Option[Long],
fromDate: ZonedDateTime = ZonedDateTime.now(), toDate: Option[ZonedDateTime] = None, timeOfDay: Int = 0,
languageId: Long = 10, findFirstFreeTerm: Boolean = true)(implicit ec: ExecutionContext): Future[AvailableTermsResponse] = {
async(syncApi.availableTerms(accessToken, tokenType, cityId, payerId, clinicId, serviceId, doctorId, fromDate, toDate, timeOfDay, languageId, findFirstFreeTerm))
}
def temporaryReservation(accessToken: String, tokenType: String, temporaryReservationRequest: TemporaryReservationRequest)(implicit ec: ExecutionContext): Future[TemporaryReservationResponse] = {
async(syncApi.temporaryReservation(accessToken, tokenType, temporaryReservationRequest))
}
def deleteTemporaryReservation(accessToken: String, tokenType: String, temporaryReservationId: Long)(implicit ec: ExecutionContext): Future[HttpResponse[String]] = {
async(syncApi.deleteTemporaryReservation(accessToken, tokenType, temporaryReservationId))
}
def valuations(accessToken: String, tokenType: String, valuationsRequest: ValuationsRequest)(implicit ec: ExecutionContext): Future[ValuationsResponse] = {
async(syncApi.valuations(accessToken, tokenType, valuationsRequest))
}
def reservation(accessToken: String, tokenType: String, reservationRequest: ReservationRequest)(implicit ec: ExecutionContext): Future[ReservationResponse] = {
async(syncApi.reservation(accessToken, tokenType, reservationRequest))
}
def deleteReservation(accessToken: String, tokenType: String, reservationId: Long)(implicit ec: ExecutionContext): Future[HttpResponse[String]] = {
async(syncApi.deleteReservation(accessToken, tokenType, reservationId))
}
private def async[T](f: => Either[Throwable, T])(implicit ec: ExecutionContext) = {
Future(f).flatMap {
case Right(r) => Future.successful(r)
case Left(ex) => Future.failed(ex)
}
}
}

View File

@@ -0,0 +1,28 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api.exception
case class LuxmedException(code: Int, status: String, message: String) extends Exception(message) {
override def toString: String = s"Code: $code, status: $status, message: $message"
}

View File

@@ -0,0 +1,101 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api
import com.lbs.api.exception.LuxmedException
import com.lbs.api.json.JsonSerializer.extensions._
import com.lbs.api.json.model.{LuxmedBaseError, LuxmedCompositeError, LuxmedError, SerializableJsonObject}
import com.lbs.common.Logger
import scalaj.http.{HttpRequest, HttpResponse}
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
package object http extends Logger {
object headers {
val `Content-Type` = "Content-Type"
val Host = "Host"
val Accept = "Accept"
val Connection = "Connection"
val `Accept-Encoding` = "Accept-Encoding"
val `User-Agent` = "User-Agent"
val `x-api-client-identifier` = "x-api-client-identifier"
val `Accept-Language` = "Accept-Language"
val Authorization = "Authorization"
}
implicit class HttpResponseWithJsonDeserializationSupport(httpResponse: HttpResponse[String]) {
def asEntity[T <: SerializableJsonObject](implicit mf: scala.reflect.Manifest[T]): HttpResponse[T] = {
httpResponse.copy(body = httpResponse.body.as[T])
}
def asEntityAsync[T <: SerializableJsonObject](implicit mf: scala.reflect.Manifest[T], ec: ExecutionContext): Future[HttpResponse[T]] = {
Future(asEntity[T])
}
}
implicit class ExtendedHttpRequest(httpRequest: HttpRequest) {
def toEither: Either[Throwable, HttpResponse[String]] = {
toTry.toEither
}
def toTry: Try[HttpResponse[String]] = {
LOG.debug(s"Sending request:\n${hidePasswords(httpRequest)}")
val httpResponse = Try(httpRequest.asString)
LOG.debug(s"Received response:\n$httpResponse")
extractLuxmedError(httpResponse) match {
case Some(error) => Try(throw error)
case None => httpResponse.map(_.throwError)
}
}
def param(key: String, value: Option[String]): HttpRequest = {
value.map(v => httpRequest.param(key, v)).getOrElse(httpRequest)
}
private def luxmedErrorToException[T <: LuxmedBaseError](ler: HttpResponse[T]) = {
ler.body match {
case e: LuxmedCompositeError =>
LuxmedException(ler.code, ler.statusLine, e.errors.map(_.message).mkString("; "))
case e: LuxmedError =>
LuxmedException(ler.code, ler.statusLine, e.message)
}
}
private def extractLuxmedError(httpResponse: Try[HttpResponse[String]]) = {
httpResponse.flatMap(response => Try(response.asEntity[LuxmedCompositeError]).map(luxmedErrorToException).
orElse(Try(response.asEntity[LuxmedError]).map(luxmedErrorToException))).toOption
}
private def hidePasswords(httpRequest: HttpRequest) = {
httpRequest.copy(params = httpRequest.params.map { case (k, v) =>
if (k.toLowerCase.contains("passw")) k -> "******" else k -> v
})
}
}
}

View File

@@ -0,0 +1,69 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api.json
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import com.lbs.api.json.model.SerializableJsonObject
import org.json4s._
import org.json4s.jackson.JsonMethods._
object JsonSerializer {
private val localDateTimeSerializer = new CustomSerializer[ZonedDateTime](_ => ( {
case JString(str) => ZonedDateTime.parse(str, DateTimeFormatter.ISO_OFFSET_DATE_TIME)
}, {
case zonedDateTime: ZonedDateTime => JString(zonedDateTime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME))
}
))
private implicit val formats: Formats = DefaultFormats.withStrictArrayExtraction + localDateTimeSerializer
def extract[T <: SerializableJsonObject](jsonString: String)(implicit mf: scala.reflect.Manifest[T]): T = {
parse(jsonString).camelizeKeys.extract[T]
}
def write[T <: SerializableJsonObject](jsonObject: T): String = {
pretty(render(Extraction.decompose(jsonObject).pascalizeKeys))
}
object extensions {
implicit class JsonStringToObject(jsonString: String) {
def as[T <: SerializableJsonObject](implicit mf: scala.reflect.Manifest[T]): T = {
extract[T](jsonString)
}
}
implicit class JsonObjectToString[T <: SerializableJsonObject](jsonObject: T) {
def asJson: String = {
write(jsonObject)
}
}
}
}

View File

@@ -0,0 +1,86 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api.json.model
/**
*
{
"AvailableVisitsTermPresentation": [
{
"Clinic": {
"Id": 6,
"Name": "LX Wrocław - Szewska 3A"
},
"Doctor": {
"Id": 38275,
"Name": "lek. med. ANNA ABRAMCZYK"
},
"Impediment": {
"ImpedimentText": "",
"IsImpediment": false
},
"IsFree": false,
"PayerDetailsList": [
{
"BrandId": 2,
"ContractId": 1111111,
"PayerId": 22222,
"PayerName": "FIRMA POLAND SP. Z O.O.",
"ProductElementId": 3333333,
"ProductId": 44444,
"ProductInContractId": 555555,
"ServaAppId": 0,
"ServaId": 6666
},
{
"BrandId": 2,
"ContractId": 1111111,
"PayerId": 22222,
"PayerName": "FIRMA POLAND SP. Z O.O.",
"ProductElementId": 8547135,
"ProductId": 44444,
"ProductInContractId": 555555,
"ServaAppId": 1,
"ServaId": 6666
}
],
"ReferralRequiredByProduct": false,
"ReferralRequiredByService": false,
"RoomId": 543,
"ScheduleId": 3331908,
"ServiceId": 6666,
"VisitDate": {
"FormattedDate": "26th April, Thu. at 12:40 pm",
"StartDateTime": "2018-02-23T11:30:00+02:00"
}
}
]
}
*/
case class AvailableTermsResponse(availableVisitsTermPresentation: List[AvailableVisitsTermPresentation]) extends SerializableJsonObject
case class AvailableVisitsTermPresentation(clinic: IdName, doctor: IdName, payerDetailsList: List[PayerDetails],
referralRequiredByProduct: Boolean, referralRequiredByService: Boolean,
roomId: Long, scheduleId: Long, serviceId: Long, visitDate: VisitDate) extends SerializableJsonObject

View File

@@ -0,0 +1,28 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api.json.model
case class IdName(id: Long, name: String) {
def optionalId: Option[Long] = Option(id).filterNot(_ == -1L)
}

View File

@@ -0,0 +1,34 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api.json.model
/**
* {
* "access_token": "IDtjG_ECOd_ETYE2fwrCoTcC6bW935cn_nUh6d3BaEa-jvPlHfPLOY5AkF",
* "expires_in": 599,
* "refresh_token": "d251c66c-49e0-4777-b766-08326d83fa31",
* "token_type": "bearer"
* }
*/
case class LoginResponse(accessToken: String, expiresIn: Int, refreshToken: String, tokenType: String) extends SerializableJsonObject

View File

@@ -0,0 +1,26 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api.json.model
trait LuxmedBaseError

View File

@@ -0,0 +1,28 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api.json.model
case class LuxmedCompositeError(errors: List[LuxmedCompositeMessage]) extends SerializableJsonObject with LuxmedBaseError
case class LuxmedCompositeMessage(errorCode: Int, message: String) extends SerializableJsonObject

View File

@@ -0,0 +1,26 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api.json.model
case class LuxmedError(message: String) extends SerializableJsonObject with LuxmedBaseError

View File

@@ -0,0 +1,27 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api.json.model
case class PayerDetails(brandId: Option[Long], contractId: Long, payerId: Long, payerName: String, productElementId: Long,
productId: Long, productInContractId: Long, servaAppId: Long, servaId: Long) extends SerializableJsonObject

View File

@@ -0,0 +1,89 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api.json.model
/**
{
"Cities": [
{
"Id": 5,
"Name": "Wrocław"
}
],
"Clinics": [
{
"Id": 1405,
"Name": "Konsylium Wrocław - Legnicka 40"
},
{
"Id": 7,
"Name": "LX Wrocław - Kwidzyńska 6"
}
],
"DefaultPayer": {
"Id": 22222,
"Name": "FIRMA POLAND SP. Z O.O."
},
"Doctors": [
{
"Id": 38275,
"Name": "ANNA ABRAMCZYK lek. med."
},
{
"Id": 15565,
"Name": "ANDRZEJ ANDEWSKI dr n. med."
}
],
"Languages": [
{
"Id": 11,
"Name": "english"
},
{
"Id": 10,
"Name": "polish"
}
],
"Payers": [
{
"Id": 22222,
"Name": "FIRMA POLAND SP. Z O.O."
}
],
"Services": [
{
"Id": 5857,
"Name": "Audiometr standardowy"
},
{
"Id": 7976,
"Name": "Audiometr standardowy - audiometria nadprogowa"
}
]
}
*/
case class ReservationFilterResponse(cities: List[IdName], clinics: List[IdName], defaultPayer: Option[IdName],
doctors: List[IdName], languages: List[IdName], payers: List[IdName],
services: List[IdName]) extends SerializableJsonObject

View File

@@ -0,0 +1,50 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api.json.model
import java.time.ZonedDateTime
/**
{
"ClinicId": 6,
"DoctorId": 38509,
"PayerData": {
"ContractId": 1111111,
"PayerId": 22222,
"PayerName": "FIRMA POLAND SP. Z O.O.",
"ProductElementId": 8547100,
"ProductId": 44444,
"ProductInContractId": 555555,
"ServaAppId": 0,
"ServaId": 6621
},
"RoomId": 159,
"ServiceId": 6621,
"StartDateTime": "2018-06-04T11:00:00+02:00",
"TemporaryReservationId": 250303839
}
*/
case class ReservationRequest(clinicId: Long, doctorId: Long, payerData: PayerDetails, roomId: Long, serviceId: Long,
startDateTime: ZonedDateTime, temporaryReservationId: Long) extends SerializableJsonObject

View File

@@ -0,0 +1,46 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api.json.model
/**
{
"PreparationInfo": {
"IsPreparationRequired": true
},
"ReservedVisitsLimitInfo": {
"CanReserve": true,
"HasPatientLimit": false,
"MaxReservedVisitsCount": null,
"Message": "",
"ReservedVisitsCount": null
}
}
*/
case class ReservationResponse(preparationInfo: PreparationInfo, reservedVisitsLimitInfo: ReservedVisitsLimitInfo) extends SerializableJsonObject
case class PreparationInfo(isPreparationRequired: Boolean) extends SerializableJsonObject
case class ReservedVisitsLimitInfo(canReserve: Boolean, hasPatientLimit: Boolean, maxReservedVisitsCount: Option[Int],
message: String, reservedVisitsCount: Option[Int]) extends SerializableJsonObject

View File

@@ -0,0 +1,66 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api.json.model
/**
*
* {
* "ReservedVisits": [
*{
*"CanBeCanceled": true,
*"Clinic": {
*"Id": 6,
*"Name": "LX Wrocław - Szewska 3A"
*},
*"DoctorName": "lek. stom. TARAS SHEVCZENKO",
*"Impediment": {
*"ImpedimentText": "",
*"IsImpediment": false
*},
*"IsAdditional": false,
*"IsPreparationRequired": false,
*"Links": [
*{
*"Href": "/PatientPortalMobileAPI/api/visits/preparations/6621",
*"Method": "GET",
*"Rel": "get_preparations"
*}
*],
*"ReservationId": 888888888,
*"Service": {
*"Id": 6621,
*"Name": "Umówienie wizyty u stomatologa"
*},
*"VisitDate": {
*"FormattedDate": "21rd May, Mon. at 3:00 pm",
*"StartDateTime": "2018-05-21T15:00:00+02:00"
*}
*}
*]
*}
*/
case class ReservedVisitsResponse(reservedVisits: List[ReservedVisit]) extends SerializableJsonObject
case class ReservedVisit(canBeCanceled: Boolean, clinic: IdName, doctorName: String,
reservationId: Long, service: IdName, visitDate: VisitDate) extends SerializableJsonObject

View File

@@ -0,0 +1,26 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api.json.model
trait SerializableJsonObject

View File

@@ -0,0 +1,65 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api.json.model
import java.time.ZonedDateTime
/**
{
"ClinicId": 6,
"DoctorId": 38275,
"PayerDetailsList": [
{
"BrandId": 2,
"ContractId": 1111111,
"PayerId": 22222,
"PayerName": "FIRMA POLAND SP. Z O.O.",
"ProductElementId": 3333333,
"ProductId": 44444,
"ProductInContractId": 555555,
"ServaAppId": 0,
"ServaId": 6666
},
{
"BrandId": 2,
"ContractId": 1111111,
"PayerId": 22222,
"PayerName": "FIRMA POLAND SP. Z O.O.",
"ProductElementId": 8547135,
"ProductId": 44444,
"ProductInContractId": 555555,
"ServaAppId": 1,
"ServaId": 6666
}
],
"ReferralRequiredByService": false,
"RoomId": 543,
"ServiceId": 6666,
"StartDateTime": "2018-02-23T11:30:00+02:00"
}
*/
case class TemporaryReservationRequest(clinicId: Long, doctorId: Long, payerDetailsList: List[PayerDetails],
referralRequiredByService: Boolean, roomId: Long, serviceId: Long,
startDateTime: ZonedDateTime) extends SerializableJsonObject

View File

@@ -0,0 +1,27 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api.json.model
case class TemporaryReservationResponse(hasReferralRequired: Boolean, id: Long,
informationMessages: List[String]) extends SerializableJsonObject

View File

@@ -0,0 +1,65 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api.json.model
import java.time.ZonedDateTime
/**
{
"ClinicId": 6,
"DoctorId": 38275,
"PayerDetailsList": [
{
"BrandId": 2,
"ContractId": 1111111,
"PayerId": 22222,
"PayerName": "FIRMA POLAND SP. Z O.O.",
"ProductElementId": 3333333,
"ProductId": 44444,
"ProductInContractId": 555555,
"ServaAppId": 0,
"ServaId": 6666
},
{
"BrandId": 2,
"ContractId": 1111111,
"PayerId": 22222,
"PayerName": "FIRMA POLAND SP. Z O.O.",
"ProductElementId": 8547135,
"ProductId": 44444,
"ProductInContractId": 555555,
"ServaAppId": 1,
"ServaId": 6666
}
],
"ReferralRequiredByService": false,
"RoomId": 543,
"ServiceId": 6666,
"StartDateTime": "2018-02-23T11:30:00+02:00"
}
*/
case class ValuationsRequest(clinicId: Long, doctorId: Long, payerDetailsList: List[PayerDetails],
referralRequiredByService: Boolean, roomId: Long, serviceId: Long,
startDateTime: ZonedDateTime) extends SerializableJsonObject

View File

@@ -0,0 +1,63 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api.json.model
/**
{
"OptionsQuestion": "Would you like to confirm your appointment booking?",
"VisitTermVariants": [
{
"CanBeReserve": true,
"InfoMessage": "During the appointment, the physician will indicate the services to be provided and will inform you of the relevant fee, if any. The services will be provided in accordance with the scope of the agreement.",
"IsStomatology": true,
"OptionMessage": "I do not have the required referral",
"PaymentMessage": "",
"ReferralRequired": false,
"ValuationDetail": {
"PayerData": {
"BrandId": null,
"ContractId": 1111111,
"PayerId": 22222,
"PayerName": "FIRMA POLAND SP. Z O.O.",
"ProductElementId": 8547100,
"ProductId": 44444,
"ProductInContractId": 555555,
"ServaAppId": 0,
"ServaId": 6621
},
"Price": 0.0,
"ValuationType": 1
},
"WarningMessage": ""
}
]
}
*/
case class ValuationsResponse(optionsQuestion: Option[String], visitTermVariants: List[VisitTermVariant]) extends SerializableJsonObject
case class VisitTermVariant(canBeReserve: Boolean, infoMessage: String, isStomatology: Boolean, optionMessage: String, paymentMessage: String,
referralRequired: Boolean, valuationDetail: ValuationDetail, warningMessage: String) extends SerializableJsonObject
case class ValuationDetail(payerData: PayerDetails, price: BigDecimal, valuationType: Int) extends SerializableJsonObject

View File

@@ -0,0 +1,28 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api.json.model
import java.time.ZonedDateTime
case class VisitDate(formattedDate: String, startDateTime: ZonedDateTime) extends SerializableJsonObject

View File

@@ -0,0 +1,99 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api.json.model
/**
{
"AreMoreVisits": false,
"HistoricVisits": [
{
"ClinicName": "LX Wrocław - Szewska 3A",
"DoctorName": "lek. stom. TARAS SHEVCZENKO",
"HasRecommendations": false,
"HasReferrals": false,
"IsAdditional": false,
"Links": [
{
"Href": "/PatientPortalMobileAPI/api/visits/recommendations/222222222",
"Method": "GET",
"Rel": "get_recommendations"
}
],
"QuestionToVisit": {
"IsAnswered": false,
"IsAsked": false,
"IsQuestionToVisitAvailable": false
},
"RateVisit": {
"IsRatingAvailable": false,
"IsVisitRated": false
},
"ReservationId": 222222222,
"Service": {
"Id": 6621,
"Name": "Umówienie wizyty u stomatologa"
},
"VisitDate": {
"FormattedDate": "17th Jan 2018, at 1:00 pm",
"StartDateTime": "2018-01-17T13:00:00+02:00"
}
},
{
"ClinicName": "LX Wrocław - Szewska 3A",
"DoctorName": "lek. stom. TARAS SHEVCZENKO",
"HasRecommendations": false,
"HasReferrals": false,
"IsAdditional": false,
"Links": [
{
"Href": "/PatientPortalMobileAPI/api/visits/recommendations/999999999",
"Method": "GET",
"Rel": "get_recommendations"
}
],
"QuestionToVisit": {
"IsAnswered": false,
"IsAsked": false,
"IsQuestionToVisitAvailable": false
},
"RateVisit": {
"IsRatingAvailable": false,
"IsVisitRated": false
},
"ReservationId": 999999999,
"Service": {
"Id": 3589,
"Name": "Wypełnienie ubytku korony zęba na 2 powierzchniach"
},
"VisitDate": {
"FormattedDate": "17th Jan 2018, at 1:00 pm",
"StartDateTime": "2018-01-17T13:00:00+02:00"
}
}
]
}
*/
case class VisitsHistoryResponse(areMoreVisits: Boolean, historicVisits: List[HistoricVisit]) extends SerializableJsonObject
case class HistoricVisit(clinicName: String, doctorName: String, reservationId: Long, service: IdName, visitDate: VisitDate)

View File

@@ -0,0 +1,66 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs
import com.lbs.api.json.model.{AvailableTermsResponse, ReservationFilterResponse, ReservedVisitsResponse, VisitsHistoryResponse}
import scala.util.matching.Regex
package object api {
object ApiResponseMutators {
private val DoctorPrefixes: Regex = """\s*(dr\s*n.\s*med.|dr\s*hab.\s*n.\s*med|lek.\s*med.|lek.\s*stom.)\s*""".r
private def cleanupDoctorName(name: String) = DoctorPrefixes.replaceFirstIn(name, "")
trait ResponseMutator[T] {
def mutate(response: T): T
}
implicit class ResponseOps[T: ResponseMutator](response: Either[Throwable, T]) {
def mutate: Either[Throwable, T] = {
val mutator = implicitly[ResponseMutator[T]]
response.map(mutator.mutate)
}
}
implicit val ReservedVisitsResponseMutator: ResponseMutator[ReservedVisitsResponse] = (response: ReservedVisitsResponse) => {
response.copy(reservedVisits = response.reservedVisits.map(rv => rv.copy(doctorName = cleanupDoctorName(rv.doctorName))))
}
implicit val VisitsHistoryResponseMutator: ResponseMutator[VisitsHistoryResponse] = (response: VisitsHistoryResponse) => {
response.copy(historicVisits = response.historicVisits.map(hv => hv.copy(doctorName = cleanupDoctorName(hv.doctorName))))
}
implicit val ReservationFilterResponseMutator: ResponseMutator[ReservationFilterResponse] = (response: ReservationFilterResponse) => {
response.copy(doctors = response.doctors.map(d => d.copy(name = cleanupDoctorName(d.name))))
}
implicit val AvailableTermsResponseMutator: ResponseMutator[AvailableTermsResponse] = (response: AvailableTermsResponse) => {
response.copy(availableVisitsTermPresentation =
response.availableVisitsTermPresentation.map(atp => atp.copy(doctor = atp.doctor.copy(name = cleanupDoctorName(atp.doctor.name)))))
}
}
}

View File

@@ -0,0 +1,43 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api.json.model
import org.scalatest.Matchers
trait CommonSpec {
_: Matchers =>
private type SimpleEntity = {val id: Long; val name: String}
protected def testSimpleEntity(simpleEntity: SimpleEntity, expectedId: Long, expectedName: String): Unit = {
simpleEntity.id should be(expectedId)
simpleEntity.name should be(expectedName)
}
protected def testSimpleEntities(simpleEntities: List[SimpleEntity], expectedSize: Int, expectedId: Long, expectedName: String): Unit = {
simpleEntities.size should be(expectedSize)
val simpleEntity = simpleEntities.head
testSimpleEntity(simpleEntity, expectedId, expectedName)
}
}

View File

@@ -0,0 +1,48 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api.json.model
import com.lbs.api.json.JsonSerializer.extensions._
import org.scalatest.{FunSuiteLike, Matchers}
class LoginResponseSpec extends FunSuiteLike with Matchers {
test("deserialization") {
val json =
"""
|{
| "access_token": "RmC6qccJMJ1uVhqJZ-6sBYdfT_LznEoGuH2di0",
| "expires_in": 599,
| "refresh_token": "7854cd0b-8545-483e-88d7-d07eda90995d",
| "token_type": "bearer"
|}
""".stripMargin
val response = json.as[LoginResponse]
response.accessToken should be("RmC6qccJMJ1uVhqJZ-6sBYdfT_LznEoGuH2di0")
response.expiresIn should be(599)
response.refreshToken should be("7854cd0b-8545-483e-88d7-d07eda90995d")
response.tokenType should be("bearer")
}
}

View File

@@ -0,0 +1,106 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api.json.model
import com.lbs.api.json.JsonSerializer.extensions._
import org.scalatest.{FunSuiteLike, Matchers}
class ReservationFilterResponseSpec extends FunSuiteLike with Matchers with CommonSpec {
test("deserialization") {
val json =
"""
|{
| "Cities": [
| {
| "Id": 5,
| "Name": "Wrocław"
| }
| ],
| "Clinics": [
| {
| "Id": 1405,
| "Name": "Legnicka 40"
| },
| {
| "Id": 7,
| "Name": "Kwidzyńska 6"
| }
| ],
| "DefaultPayer": {
| "Id": 22222,
| "Name": "FIRMA"
| },
| "Doctors": [
| {
| "Id": 38275,
| "Name": "ANNA ABRAMCZYK"
| },
| {
| "Id": 15565,
| "Name": "ANDRZEJ ANDEWSKI"
| }
| ],
| "Languages": [
| {
| "Id": 11,
| "Name": "english"
| },
| {
| "Id": 10,
| "Name": "polish"
| }
| ],
| "Payers": [
| {
| "Id": 22222,
| "Name": "FIRMA"
| }
| ],
| "Services": [
| {
| "Id": 5857,
| "Name": "Audiometr standardowy"
| },
| {
| "Id": 7976,
| "Name": "Audiometr standardowy - audiometria nadprogowa"
| }
| ]
|}
""".stripMargin
val response = json.as[ReservationFilterResponse]
testSimpleEntities(response.cities, 1, 5L, "Wrocław")
testSimpleEntities(response.clinics, 2, 1405L, "Legnicka 40")
response.defaultPayer should be (_: Some[IdName])
testSimpleEntity(response.defaultPayer.get, 22222L, "FIRMA")
testSimpleEntities(response.doctors, 2, 38275L, "ANNA ABRAMCZYK")
testSimpleEntities(response.languages, 2, 11L, "english")
testSimpleEntities(response.payers, 1, 22222L, "FIRMA")
testSimpleEntities(response.services, 2, 5857L, "Audiometr standardowy")
}
}

View File

@@ -0,0 +1,84 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api.json.model
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import com.lbs.api.json.JsonSerializer.extensions._
import org.scalatest.{FunSuiteLike, Matchers}
class ReservedVisitsResponseSpec extends FunSuiteLike with Matchers with CommonSpec {
test("deserialization") {
val json =
"""
|{
| "ReservedVisits": [
| {
| "CanBeCanceled": true,
| "Clinic": {
| "Id": 6,
| "Name": "Szewska 3A"
| },
| "DoctorName": "TARAS SHEVCZENKO",
| "Impediment": {
| "ImpedimentText": "",
| "IsImpediment": false
| },
| "IsAdditional": false,
| "IsPreparationRequired": false,
| "Links": [
| {
| "Href": "/PatientPortalMobileAPI/api/visits/preparations/6621",
| "Method": "GET",
| "Rel": "get_preparations"
| }
| ],
| "ReservationId": 888888888,
| "Service": {
| "Id": 6621,
| "Name": "stomatolog"
| },
| "VisitDate": {
| "FormattedDate": "21rd May, Mon. at 3:00 pm",
| "StartDateTime": "2018-05-21T15:00:00+02:00"
| }
| }
| ]
|}
""".stripMargin
val response = json.as[ReservedVisitsResponse]
response.reservedVisits.size should be(1)
val reservedVisit = response.reservedVisits.head
reservedVisit.canBeCanceled should be(true)
testSimpleEntity(reservedVisit.clinic, 6L, "Szewska 3A")
reservedVisit.doctorName should be("TARAS SHEVCZENKO")
reservedVisit.reservationId should be(888888888L)
testSimpleEntity(reservedVisit.service, 6621L, "stomatolog")
reservedVisit.visitDate.formattedDate should be("21rd May, Mon. at 3:00 pm")
reservedVisit.visitDate.startDateTime should be(ZonedDateTime.parse("2018-05-21T15:00:00+02:00", DateTimeFormatter.ISO_OFFSET_DATE_TIME))
}
}

View File

@@ -0,0 +1,71 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api.json.model
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import com.lbs.api.json.JsonSerializer.extensions._
import org.scalatest.{FunSuiteLike, Matchers}
class TemporaryReservationRequestSpec extends FunSuiteLike with Matchers with CommonSpec {
test("serialization") {
val json =
"""
|{
| "ClinicId": 6,
| "DoctorId": 38275,
| "PayerDetailsList": [
| {
| "BrandId": 2,
| "ContractId": 1111111,
| "PayerId": 22222,
| "PayerName": "FIRMA",
| "ProductElementId": 3333333,
| "ProductId": 44444,
| "ProductInContractId": 555555,
| "ServaAppId": 0,
| "ServaId": 6666
| }
| ],
| "ReferralRequiredByService": false,
| "RoomId": 543,
| "ServiceId": 6666,
| "StartDateTime": "2018-02-23T11:30:00+02:00"
|}
""".stripMargin
val request = TemporaryReservationRequest(clinicId = 6L, doctorId = 38275L, payerDetailsList = List(
PayerDetails(brandId = Some(2L), contractId = 1111111L, payerId = 22222L, payerName = "FIRMA",
productElementId = 3333333L, productId = 44444L, productInContractId = 555555L, servaAppId = 0L, servaId = 6666L)
), referralRequiredByService = false, roomId = 543L, serviceId = 6666L,
startDateTime = ZonedDateTime.parse("2018-02-23T11:30:00+02:00", DateTimeFormatter.ISO_OFFSET_DATE_TIME))
val requestJson = request.asJson
val requestActual = requestJson.as[TemporaryReservationRequest]
val requestExpected = json.as[TemporaryReservationRequest]
requestActual should be (requestExpected)
}
}

View File

@@ -0,0 +1,71 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api.json.model
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import com.lbs.api.json.JsonSerializer.extensions._
import org.scalatest.{FunSuiteLike, Matchers}
class ValuationsRequestSpec extends FunSuiteLike with Matchers with CommonSpec {
test("serialization") {
val json =
"""
|{
| "ClinicId": 6,
| "DoctorId": 38275,
| "PayerDetailsList": [
| {
| "BrandId": 2,
| "ContractId": 1111111,
| "PayerId": 22222,
| "PayerName": "FIRMA",
| "ProductElementId": 3333333,
| "ProductId": 44444,
| "ProductInContractId": 555555,
| "ServaAppId": 0,
| "ServaId": 6666
| }
| ],
| "ReferralRequiredByService": false,
| "RoomId": 543,
| "ServiceId": 6666,
| "StartDateTime": "2018-02-23T11:30:00+02:00"
|}
""".stripMargin
val request = ValuationsRequest(clinicId = 6L, doctorId = 38275L, payerDetailsList = List(
PayerDetails(brandId = Some(2L), contractId = 1111111L, payerId = 22222L, payerName = "FIRMA",
productElementId = 3333333L, productId = 44444L, productInContractId = 555555L, servaAppId = 0L, servaId = 6666L)
), referralRequiredByService = false, roomId = 543L, serviceId = 6666L,
startDateTime = ZonedDateTime.parse("2018-02-23T11:30:00+02:00", DateTimeFormatter.ISO_OFFSET_DATE_TIME))
val requestJson = request.asJson
val requestActual = requestJson.as[ValuationsRequest]
val requestExpected = json.as[ValuationsRequest]
requestActual should be (requestExpected)
}
}

View File

@@ -0,0 +1,119 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.api.json.model
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import com.lbs.api.json.JsonSerializer.extensions._
import org.scalatest.{FunSuiteLike, Matchers}
class VisitsHistoryResponseSpec extends FunSuiteLike with Matchers with CommonSpec {
test("deserialization") {
val json =
"""
|{
| "AreMoreVisits": false,
| "HistoricVisits": [
| {
| "ClinicName": "Szewska 3A",
| "DoctorName": "TARAS SHEVCZENKO",
| "HasRecommendations": false,
| "HasReferrals": false,
| "IsAdditional": false,
| "Links": [
| {
| "Href": "/PatientPortalMobileAPI/api/visits/recommendations/222222222",
| "Method": "GET",
| "Rel": "get_recommendations"
| }
| ],
| "QuestionToVisit": {
| "IsAnswered": false,
| "IsAsked": false,
| "IsQuestionToVisitAvailable": false
| },
| "RateVisit": {
| "IsRatingAvailable": false,
| "IsVisitRated": false
| },
| "ReservationId": 222222222,
| "Service": {
| "Id": 6621,
| "Name": "stomatolog"
| },
| "VisitDate": {
| "FormattedDate": "17th Jan 2018, at 1:00 pm",
| "StartDateTime": "2018-01-17T13:00:00+02:00"
| }
| },
| {
| "ClinicName": "LX Wrocław - Szewska 3A",
| "DoctorName": "lek. stom. TARAS SHEVCZENKO",
| "HasRecommendations": false,
| "HasReferrals": false,
| "IsAdditional": false,
| "Links": [
| {
| "Href": "/PatientPortalMobileAPI/api/visits/recommendations/999999999",
| "Method": "GET",
| "Rel": "get_recommendations"
| }
| ],
| "QuestionToVisit": {
| "IsAnswered": false,
| "IsAsked": false,
| "IsQuestionToVisitAvailable": false
| },
| "RateVisit": {
| "IsRatingAvailable": false,
| "IsVisitRated": false
| },
| "ReservationId": 999999999,
| "Service": {
| "Id": 3589,
| "Name": "Wypełnienie ubytku korony zęba na 2 powierzchniach"
| },
| "VisitDate": {
| "FormattedDate": "17th Jan 2018, at 1:00 pm",
| "StartDateTime": "2018-01-17T13:00:00+02:00"
| }
| }
| ]
|}
""".stripMargin
val response = json.as[VisitsHistoryResponse]
response.areMoreVisits should be(false)
response.historicVisits.size should be(2)
val historicVisit = response.historicVisits.head
historicVisit.clinicName should be("Szewska 3A")
historicVisit.doctorName should be("TARAS SHEVCZENKO")
historicVisit.reservationId should be(222222222L)
testSimpleEntity(historicVisit.service, 6621L, "stomatolog")
historicVisit.visitDate.formattedDate should be("17th Jan 2018, at 1:00 pm")
historicVisit.visitDate.startDateTime should be(ZonedDateTime.parse("2018-01-17T13:00:00+02:00", DateTimeFormatter.ISO_OFFSET_DATE_TIME))
}
}

5
bot/build.gradle Normal file
View File

@@ -0,0 +1,5 @@
dependencies {
compile project(':common')
compile group: "info.mukel", name: "telegrambot4s_2.12", version: "3.0.14"
}

View File

@@ -0,0 +1,49 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.bot
import com.lbs.bot.model._
import com.lbs.bot.telegram.TelegramBot
import com.lbs.common.Logger
class Bot(telegram: TelegramBot /* other bots */) extends Logger {
def sendMessage(source: MessageSource, text: String): Unit =
resolveAdapter(source).sendMessage(source.chatId, text)
def sendMessage(source: MessageSource, text: String, inlineKeyboard: Option[InlineKeyboard] = None): Unit =
resolveAdapter(source).sendMessage(source.chatId, text, inlineKeyboard)
def sendEditMessage(source: MessageSource, messageId: String, inlineKeyboard: Option[InlineKeyboard]): Unit =
resolveAdapter(source).sendEditMessage(source.chatId, messageId, inlineKeyboard)
def sendEditMessage(source: MessageSource, messageId: String, text: String, inlineKeyboard: Option[InlineKeyboard] = None): Unit =
resolveAdapter(source).sendEditMessage(source.chatId, messageId, text, inlineKeyboard)
private def resolveAdapter(source: MessageSource): PollBot[_] =
source.sourceSystem match {
case TelegramMessageSourceSystem => telegram
case sourceSystem =>
sys.error(s"Unsupported source system $sourceSystem")
}
}

View File

@@ -0,0 +1,38 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.bot
import com.lbs.bot.model.{Event, InlineKeyboard}
trait PollBot[In <: Event] {
def sendMessage(chatId: String, text: String): Unit
def sendMessage(chatId: String, text: String, buttons: Option[InlineKeyboard] = None): Unit
def sendEditMessage(chatId: String, messageId: String, buttons: Option[InlineKeyboard]): Unit
def sendEditMessage(chatId: String, messageId: String, text: String, buttons: Option[InlineKeyboard] = None): Unit
protected def onReceive(command: In): Unit
}

View File

@@ -0,0 +1,30 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.bot
import com.lbs.bot.model.Event
trait WebhookBot[In <: Event] extends PollBot[In] {
def processPayload(payload: String, signature: Option[String]): Unit
}

View File

@@ -0,0 +1,38 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.bot.model
object Button {
def apply(label: String, id: Long) = new TaggedButton(label, id.toString)
def apply(label: String, id: String) = new TaggedButton(label, id)
def apply(label: String) = new LabeledButton(label)
}
trait Button
class TaggedButton(val label: String, val tag: String) extends Button
class LabeledButton(val label: String) extends Button

View File

@@ -0,0 +1,28 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.bot.model
case class Message(messageId: String, text: Option[String] = None)
case class Command(source: MessageSource, message: Message, callbackData: Option[String] = None)

View File

@@ -0,0 +1,26 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.bot.model
trait Event

View File

@@ -0,0 +1,26 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.bot.model
case class InlineKeyboard(buttons: Seq[Seq[Button]])

View File

@@ -0,0 +1,26 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.bot.model
case class MessageSource(sourceSystem: MessageSourceSystem, chatId: String)

View File

@@ -0,0 +1,57 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.bot.model
trait MessageSourceSystem {
def id: Long
def name: String
override def toString: String = name
}
object MessageSourceSystem {
val MessageSourceSystems: Seq[MessageSourceSystem] = Seq(
TelegramMessageSourceSystem,
FacebookMessageSourceSystem
)
private val MessageSourceSystemsMap = MessageSourceSystems.map(e => e.id -> e).toMap
def apply(id: Long): MessageSourceSystem = {
MessageSourceSystemsMap.getOrElse(id, sys.error(s"Unsupported source system $id"))
}
}
object TelegramMessageSourceSystem extends MessageSourceSystem {
override def id: Long = 1
override def name: String = "Telegram"
}
object FacebookMessageSourceSystem extends MessageSourceSystem {
override def id: Long = 2
override def name: String = "Facebook"
}

View File

@@ -0,0 +1,36 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs
import com.lbs.bot.model.{Button, InlineKeyboard}
package object bot {
def createInlineKeyboard(buttons: Seq[Button], columns: Int = 2): Option[InlineKeyboard] = {
Option(buttons).filterNot(_.isEmpty).map(b => InlineKeyboard(b.grouped(columns).toSeq))
}
def createInlineKeyboard(buttons: Seq[Seq[Button]]): Option[InlineKeyboard] = {
Option(buttons).filterNot(_.isEmpty).map(InlineKeyboard)
}
}

View File

@@ -0,0 +1,51 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.bot.telegram
import com.lbs.bot.PollBot
import com.lbs.bot.model._
import com.lbs.bot.telegram.TelegramModelConverters._
import info.mukel.telegrambot4s.models.InlineKeyboardMarkup
class TelegramBot(onCommand: Command => Unit, botToken: String) extends PollBot[TelegramEvent] {
private val telegramBot = new TelegramClient(onReceive, botToken)
telegramBot.run()
def sendMessage(chatId: String, text: String): Unit =
telegramBot.sendMessage(chatId.toLong, text)
def sendMessage(chatId: String, text: String, buttons: Option[InlineKeyboard] = None): Unit =
telegramBot.sendMessage(chatId.toLong, text, replyMarkup = buttons.map(_.mapTo[InlineKeyboardMarkup]))
def sendEditMessage(chatId: String, messageId: String, buttons: Option[InlineKeyboard]): Unit =
telegramBot.sendEditMessage(chatId.toLong, messageId.toInt, replyMarkup = buttons.map(_.mapTo[InlineKeyboardMarkup]))
def sendEditMessage(chatId: String, messageId: String, text: String, buttons: Option[InlineKeyboard] = None): Unit =
telegramBot.sendEditMessage(chatId.toLong, messageId.toInt, text, replyMarkup = buttons.map(_.mapTo[InlineKeyboardMarkup]))
override protected def onReceive(command: TelegramEvent): Unit = {
onCommand(command.mapTo[Command])
}
}

View File

@@ -0,0 +1,66 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.bot.telegram
import com.lbs.common.Logger
import info.mukel.telegrambot4s.api.declarative.{Callbacks, Commands}
import info.mukel.telegrambot4s.api.{Polling, TelegramBot => TelegramBotBase}
import info.mukel.telegrambot4s.methods.{EditMessageReplyMarkup, EditMessageText, ParseMode, SendMessage}
import info.mukel.telegrambot4s.models._
import scala.concurrent.Future
class TelegramClient(onReceive: TelegramEvent => Unit, botToken: String) extends TelegramBotBase with Polling with Commands with Callbacks with Logger {
override def token: String = botToken
def sendMessage(chatId: Long, text: String): Future[Message] =
request(SendMessage(chatId, text, parseMode = Some(ParseMode.HTML)))
def sendMessage(chatId: Long, text: String, replyMarkup: Option[InlineKeyboardMarkup] = None): Future[Message] =
request(SendMessage(chatId, text, parseMode = Some(ParseMode.HTML), replyMarkup = replyMarkup))
def sendEditMessage(chatId: Long, messageId: Int, replyMarkup: Option[InlineKeyboardMarkup]): Future[Either[Boolean, Message]] =
request(EditMessageReplyMarkup(Some(chatId), Some(messageId), replyMarkup = replyMarkup))
def sendEditMessage(chatId: Long, messageId: Int, text: String, replyMarkup: Option[InlineKeyboardMarkup] = None): Future[Either[Boolean, Message]] =
request(EditMessageText(Some(chatId), Some(messageId), text = text, parseMode = Some(ParseMode.HTML), replyMarkup = replyMarkup))
override def receiveMessage(msg: Message): Unit = {
LOG.debug(s"Received telegram message: $msg")
onReceive(TelegramEvent(msg, None))
}
onCallbackWithTag(TagPrefix) { implicit cbq =>
LOG.debug(s"Received telegram callback: $cbq")
ackCallback()
for {
data <- cbq.data.map(_.stripPrefix(TagPrefix))
msg <- cbq.message
} {
onReceive(TelegramEvent(msg, Some(data)))
}
}
}

View File

@@ -0,0 +1,29 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.bot.telegram
import com.lbs.bot.model.Event
import info.mukel.telegrambot4s.models.Message
case class TelegramEvent(msg: Message, callbackData: Option[String]) extends Event

View File

@@ -0,0 +1,77 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.bot
import com.lbs.bot.model._
import com.lbs.common.ModelConverters
import info.mukel.telegrambot4s.models
import info.mukel.telegrambot4s.models.{InlineKeyboardButton, InlineKeyboardMarkup}
package object telegram {
protected[bot] val TagPrefix = "callback"
object TelegramModelConverters extends ModelConverters {
implicit val TelegramCommandToCommandConverter:
ObjectConverter[TelegramEvent, Command] =
new ObjectConverter[TelegramEvent, Command] {
override def convert[Z <: TelegramEvent](data: Z): Command = {
Command(
source = MessageSource(TelegramMessageSourceSystem, data.msg.chat.id.toString),
message = Message(data.msg.messageId.toString, data.msg.text),
callbackData = data.callbackData
)
}
}
implicit val TelegramMessageToMessageConverter:
ObjectConverter[models.Message, Message] =
new ObjectConverter[models.Message, Message] {
override def convert[Z <: models.Message](data: Z): Message = {
Message(data.messageId.toString, data.text)
}
}
implicit val InlineKeyboardToInlineKeyboardMarkup:
ObjectConverter[InlineKeyboard, InlineKeyboardMarkup] =
new ObjectConverter[InlineKeyboard, InlineKeyboardMarkup] {
override def convert[Z <: InlineKeyboard](inlineKeyboard: Z): InlineKeyboardMarkup = {
val buttons = inlineKeyboard.buttons.map { row =>
row.map(createInlineKeyboardButton)
}
InlineKeyboardMarkup(buttons)
}
}
private def createInlineKeyboardButton(button: Button) = {
button match {
case b: TaggedButton => InlineKeyboardButton.callbackData(b.label, tag(b.tag))
case b: LabeledButton => InlineKeyboardButton.callbackData(b.label, b.label)
}
}
private def tag(name: String): String = TagPrefix + name
}
}

47
build.gradle Normal file
View File

@@ -0,0 +1,47 @@
group = 'com.lbs'
version = '0.0.1-SNAPSHOT'
ext {
scalaVersion = "2.12.6"
}
apply plugin: 'idea'
subprojects {
idea {
module {
downloadSources = true
}
}
repositories {
mavenCentral()
jcenter()
maven { url 'https://repo.spring.io/libs-milestone' }
}
apply plugin: 'java'
apply plugin: 'scala'
dependencies {
compile group: 'org.scala-lang', name: "scala-library", version: scalaVersion
compile('org.scalaz:scalaz-core_2.12:7.2.23')
testCompile('org.scalatest:scalatest_2.12:3.0.4')
testCompile('org.mockito:mockito-core:2.13.0')
testCompile('org.pegdown:pegdown:1.6.0')
}
task scalaTest(dependsOn: ['testClasses'], type: JavaExec) {
main = 'org.scalatest.tools.Runner'
args = ['-R', "build/classes/test",
'-u', "build/test-results/$name",
'-h', "build/resports/scalaTests/$name",
'-o'
]
classpath = sourceSets.test.runtimeClasspath
}
test.dependsOn scalaTest
}

5
common/build.gradle Normal file
View File

@@ -0,0 +1,5 @@
dependencies {
compile group: "org.slf4j", name: "slf4j-api", version: "1.7.25"
compile group: "ch.qos.logback", name: "logback-classic", version: "1.2.3"
compile group: "ch.qos.logback", name: "logback-core", version: "1.2.3"
}

View File

@@ -0,0 +1,38 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.common
import java.util.Optional
import scala.language.implicitConversions
object Implicits {
implicit def optionalToOption[T](optional: Optional[T]): Option[T] = {
Option(optional.orElse(null.asInstanceOf[T]))
}
implicit def optionToOptional[T](option: Option[T]): Optional[T] = {
Optional.of(option.getOrElse(null.asInstanceOf[T]))
}
}

View File

@@ -0,0 +1,66 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.common
import org.slf4j
import org.slf4j.LoggerFactory
trait Logger {
private val log: slf4j.Logger = LoggerFactory.getLogger(this.getClass)
protected val LOG = new LoggerWrapper
class LoggerWrapper {
def debug(msg: => String): Unit = {
if (log.isDebugEnabled)
log.debug(msg)
}
def warn(msg: => String): Unit = {
if (log.isWarnEnabled)
log.warn(msg)
}
def warn(msg: => String, throwable: Throwable): Unit = {
if (log.isWarnEnabled)
log.warn(msg, throwable)
}
def error(msg: => String): Unit = {
if (log.isErrorEnabled)
log.error(msg)
}
def error(msg: => String, throwable: Throwable): Unit = {
if (log.isErrorEnabled)
log.error(msg, throwable)
}
def info(msg: => String): Unit = {
if (log.isInfoEnabled)
log.info(msg)
}
}
}

View File

@@ -0,0 +1,47 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.common
import scala.collection.generic.CanBuildFrom
import scala.language.{higherKinds, implicitConversions}
trait ModelConverters {
trait CollectionConverter[-In, Out] {
def convert[Z <: In, Col[X] <: Iterable[X]](col: Col[Z])(implicit bf: CanBuildFrom[Col[Z], Out, Col[Out]]): Col[Out]
}
trait ObjectConverter[-In, Out] {
def convert[Z <: In](any: Z): Out
}
implicit class CollectionOps[From, Col[X] <: Iterable[X]](col: Col[From]) {
def mapTo[To](implicit converter: CollectionConverter[From, To], bf: CanBuildFrom[Col[From], To, Col[To]]): Col[To] = converter.convert(col)
}
implicit class ObjectOps[From](anyRef: From) {
def mapTo[To](implicit converter: ObjectConverter[From, To]): To = converter.convert(anyRef)
}
}

View File

@@ -0,0 +1,32 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.common
import java.util.concurrent.ConcurrentHashMap
class ParametrizedLock[K] {
private val locks = new ConcurrentHashMap[K, AnyRef]
def obtainLock(key: K): AnyRef = locks.computeIfAbsent(key, k => new AnyRef)
}

View File

@@ -0,0 +1,41 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.common
import java.util.concurrent.{Executors, ScheduledFuture}
import scala.concurrent.duration.FiniteDuration
class Scheduler(poolSize: Int) {
private val scheduledThreadPool = Executors.newScheduledThreadPool(poolSize)
def schedule(fn: => Unit, period: FiniteDuration): ScheduledFuture[_] = {
scheduledThreadPool.scheduleAtFixedRate(() => fn, period.length, period.length, period.unit)
}
def schedule(fn: => Unit, delay: FiniteDuration, period: FiniteDuration): ScheduledFuture[_] = {
require(delay.unit == period.unit, s"Delay units must be the same as for period ${period.unit}")
scheduledThreadPool.scheduleAtFixedRate(() => fn, delay.length, period.length, period.unit)
}
}

5
gradle.properties Normal file
View File

@@ -0,0 +1,5 @@
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.configureondemand=true
org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,6 @@
#Thu Feb 01 18:20:57 CET 2018
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip

172
gradlew vendored Executable file
View File

@@ -0,0 +1,172 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save ( ) {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

84
gradlew.bat vendored Normal file
View File

@@ -0,0 +1,84 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

31
server/build.gradle Normal file
View File

@@ -0,0 +1,31 @@
buildscript {
ext {
springBootVersion = '2.0.2.RELEASE'
}
repositories {
mavenCentral()
jcenter()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'scala'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
dependencies {
compile project(':api')
compile project(':bot')
compile project(':common')
compile('org.springframework.boot:spring-boot-starter')
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.jasypt:jasypt:1.9.2')
compile('org.postgresql:postgresql:42.2.1.jre7')
}

View File

@@ -0,0 +1,3 @@
akka {
loglevel = "WARNING"
}

View File

@@ -0,0 +1,31 @@
spring:
datasource:
url: "jdbc:postgresql://127.0.0.1:5432/lbs"
username: "lbs"
password: "lsb123"
jpa:
properties:
hibernate:
temp:
use_jdbc_metadata_defaults: "false"
database-platform: "org.hibernate.dialect.PostgreSQL9Dialect"
generate-ddl: "true"
# hibernate:
# ddl-auto: "create-update"
# ddl-auto: "create-drop"
banner:
location: "classpath:/banner.txt"
logging:
file: logs/app.log
level:
com.lbs: DEBUG
# org.hibernate.SQL: DEBUG
# org.hibernate.type.descriptor.sql.BasicBinder: TRACE
pattern:
file: "%d{yyyy-MM-dd HH:mm:ss} %logger{25} - %msg%n"
console: "%d{yyyy-MM-dd HH:mm:ss} %logger{25} - %msg%n"
security.secret: ${SECURITY_SECRET:random_secret_hfjdsk72euhdsbcgg6}
telegram.token: ${TELEGRAM_TOKEN}

View File

@@ -0,0 +1,6 @@
,--. ,-----. ,---.
| | | |) /_ ' .-'
| | | .-. \ `. `-.
| '--. | '--' / .-' |
`-----' `------' `-----'

View File

@@ -0,0 +1,35 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
@SpringBootApplication
class Boot
object Boot extends App {
SpringApplication.run(classOf[Boot], args: _*)
}

View File

@@ -0,0 +1,148 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server
import akka.actor.{ActorRef, ActorSystem}
import com.lbs.bot.Bot
import com.lbs.bot.model.MessageSource
import com.lbs.bot.telegram.TelegramBot
import com.lbs.server.actor.Login.UserId
import com.lbs.server.actor._
import com.lbs.server.lang.Localization
import com.lbs.server.service.{ApiService, DataService, MonitoringService}
import org.jasypt.util.text.{StrongTextEncryptor, TextEncryptor}
import org.springframework.beans.factory.annotation.{Autowired, Value}
import org.springframework.context.annotation.{Bean, Configuration}
@Configuration
class BootConfig {
@Value("${security.secret}")
private var secret: String = _
@Value("${telegram.token}")
private var telegramBotToken: String = _
@Autowired
private var apiService: ApiService = _
@Autowired
private var dataService: DataService = _
@Autowired
private var monitoringService: MonitoringService = _
@Autowired
private var localization: Localization = _
@Bean
def actorSystem = ActorSystem()
@Bean
def textEncryptor: TextEncryptor = {
val encryptor = new StrongTextEncryptor
encryptor.setPassword(secret)
encryptor
}
@Bean
def authActorFactory: MessageSource => ActorRef = source => actorSystem.actorOf(Auth.props(source,
dataService, unauthorizedHelpActorFactory, loginActorFactory, chatActorFactory))
@Bean
def loginActorFactory: (MessageSource, ActorRef) => ActorRef = (source, originator) => actorSystem.actorOf(Login.props(source, bot,
dataService, apiService, textEncryptor, localization, originator))
@Bean
def bookingActorFactory: UserId => ActorRef = userId => actorSystem.actorOf(Book.props(userId, bot, apiService, dataService,
monitoringService, localization, datePickerFactory, staticDataActorFactory, termsPagerActorFactory))
@Bean
def unauthorizedHelpActorFactory: MessageSource => ActorRef = source => actorSystem.actorOf(UnauthorizedHelp.props(source, bot))
@Bean
def helpActorFactory: UserId => ActorRef = userId => actorSystem.actorOf(Help.props(userId, bot, localization))
@Bean
def monitoringsActorFactory: UserId => ActorRef =
userId => actorSystem.actorOf(Monitorings.props(userId, bot, monitoringService, localization, monitoringsPagerActorFactory))
@Bean
def historyActorFactory: UserId => ActorRef =
userId => actorSystem.actorOf(History.props(userId, bot, apiService, localization, historyPagerActorFactory))
@Bean
def visitsActorFactory: UserId => ActorRef =
userId => actorSystem.actorOf(Visits.props(userId, bot, apiService, localization, visitsPagerActorFactory))
@Bean
def bugActorFactory: UserId => ActorRef =
userId => actorSystem.actorOf(Bug.props(userId, bot, dataService, bugPagerActorFactory, localization))
@Bean
def settingsActorFactory: UserId => ActorRef =
userId => actorSystem.actorOf(Settings.props(userId, bot, dataService, localization))
@Bean
def chatActorFactory: UserId => ActorRef =
userId => actorSystem.actorOf(Chat.props(userId, dataService, monitoringService, bookingActorFactory, helpActorFactory,
monitoringsActorFactory, historyActorFactory, visitsActorFactory, settingsActorFactory, bugActorFactory))
@Bean
def datePickerFactory: (UserId, ActorRef) => ActorRef = (userId, originator) =>
actorSystem.actorOf(DatePicker.props(userId, bot, localization, originator))
@Bean
def staticDataActorFactory: (UserId, ActorRef) => ActorRef = (userId, originator) =>
actorSystem.actorOf(StaticData.props(userId, bot, localization, originator))
@Bean
def termsPagerActorFactory: (UserId, ActorRef) => ActorRef = (userId, originator) =>
actorSystem.actorOf(Pagers(userId, bot, localization).termsPagerProps(originator))
@Bean
def visitsPagerActorFactory: (UserId, ActorRef) => ActorRef = (userId, originator) =>
actorSystem.actorOf(Pagers(userId, bot, localization).visitsPagerProps(originator))
@Bean
def bugPagerActorFactory: (UserId, ActorRef) => ActorRef = (userId, originator) =>
actorSystem.actorOf(Pagers(userId, bot, localization).bugPagerProps(originator))
@Bean
def historyPagerActorFactory: (UserId, ActorRef) => ActorRef = (userId, originator) =>
actorSystem.actorOf(Pagers(userId, bot, localization).historyPagerProps(originator))
@Bean
def monitoringsPagerActorFactory: (UserId, ActorRef) => ActorRef = (userId, originator) =>
actorSystem.actorOf(Pagers(userId, bot, localization).monitoringsPagerProps(originator))
@Bean
def router: ActorRef = actorSystem.actorOf(Router.props(authActorFactory))
@Bean
def telegram: TelegramBot = new TelegramBot(router ! _, telegramBotToken)
@Bean
def bot: Bot = new Bot(telegram)
}

View File

@@ -0,0 +1,104 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.actor
import akka.actor.{Actor, ActorRef, PoisonPill, Props}
import com.lbs.bot.model.{Command, MessageSource}
import com.lbs.common.Logger
import com.lbs.server.actor.Chat.Init
import com.lbs.server.actor.Login.{LoggedIn, UserId}
import com.lbs.server.service.DataService
import com.lbs.server.util.MessageExtractors._
class Auth(val source: MessageSource, dataService: DataService, unauthorizedHelpActorFactory: MessageSource => ActorRef,
loginActorFactory: (MessageSource, ActorRef) => ActorRef, chatActorFactory: UserId => ActorRef) extends Actor with Logger {
private val loginActor = loginActorFactory(source, self)
private val unauthorizedHelpActor: ActorRef = unauthorizedHelpActorFactory(source)
private var userId: Option[UserId] = getUserId
private var chatActor: ActorRef = _
override def receive: Receive = {
case cmd@Command(_, Text("/help"), _) if userId.isEmpty =>
unauthorizedHelpActor ! cmd
case cmd@Command(_, Text("/start"), _) if userId.isEmpty =>
unauthorizedHelpActor ! cmd
case cmd@Command(_, Text("/login"), _) =>
userId = None
loginActor ! Init
loginActor ! cmd
case cmd: Command if userId.isEmpty =>
loginActor ! cmd
case cmd: Command if userId.nonEmpty =>
chatActor = getChatActor(userId.get)
chatActor ! cmd
case LoggedIn(forwardCommand, id) =>
val uId = UserId(id, source)
val cmd = forwardCommand.cmd
userId = Some(uId)
chatActor = getChatActor(uId, reinit = true)
if (!cmd.message.text.contains("/login"))
chatActor ! cmd
case cmd: Command =>
chatActor ! cmd
}
private def getChatActor(userId: UserId, reinit: Boolean = false): ActorRef = {
if (chatActor == null) {
chatActorFactory(userId)
} else {
if (reinit) {
chatActor ! PoisonPill
chatActorFactory(userId)
} else chatActor
}
}
def getUserId: Option[UserId] = {
val userIdMaybe = dataService.findUserIdBySource(source)
userIdMaybe.map(id => UserId(id, source))
}
override def postStop(): Unit = {
loginActor ! PoisonPill
unauthorizedHelpActor ! PoisonPill
if (chatActor != null) chatActor ! PoisonPill
}
}
object Auth {
def props(source: MessageSource, dataService: DataService, unauthorizedHelpActorFactory: MessageSource => ActorRef,
loginActorFactory: (MessageSource, ActorRef) => ActorRef, chatActorFactory: UserId => ActorRef): Props =
Props(classOf[Auth], source, dataService, unauthorizedHelpActorFactory, loginActorFactory, chatActorFactory)
}

View File

@@ -0,0 +1,342 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.actor
import java.time.ZonedDateTime
import akka.actor.{ActorRef, PoisonPill, Props}
import com.lbs.api.json.model._
import com.lbs.bot._
import com.lbs.bot.model.{Button, Command}
import com.lbs.server.actor.Book._
import com.lbs.server.actor.Chat.Init
import com.lbs.server.actor.DatePicker.{DateFromMode, DateToMode}
import com.lbs.server.actor.Login.UserId
import com.lbs.server.actor.StaticData.StaticDataConfig
import com.lbs.server.lang.{Localizable, Localization}
import com.lbs.server.repository.model.Monitoring
import com.lbs.server.service.{ApiService, DataService, MonitoringService}
import com.lbs.server.util.ServerModelConverters._
import scala.util.{Failure, Success, Try}
class Book(val userId: UserId, bot: Bot, apiService: ApiService, dataService: DataService, monitoringService: MonitoringService,
val localization: Localization, datePickerActorFactory: (UserId, ActorRef) => ActorRef, staticDataActorFactory: (UserId, ActorRef) => ActorRef,
termsPagerActorFactory: (UserId, ActorRef) => ActorRef) extends SafeFSM[FSMState, FSMData] with StaticDataForBooking with Localizable {
private val datePicker = datePickerActorFactory(userId, self)
protected val staticData = staticDataActorFactory(userId, self)
private val termsPager = termsPagerActorFactory(userId, self)
startWith(RequestCity, BookingData())
requestStaticData(RequestCity, AwaitCity, cityConfig) { bd: BookingData =>
withFunctions(
latestOptions = dataService.getLatestCities(userId.userId),
staticOptions = apiService.getAllCities(userId.userId),
applyId = id => bd.copy(cityId = id))
}(requestNext = RequestClinic)
requestStaticData(RequestClinic, AwaitClinic, clinicConfig) { bd: BookingData =>
withFunctions(
latestOptions = dataService.getLatestClinicsByCityId(userId.userId, bd.cityId.id),
staticOptions = apiService.getAllClinics(userId.userId, bd.cityId.id),
applyId = id => bd.copy(clinicId = id))
}(requestNext = RequestService)
requestStaticData(RequestService, AwaitService, serviceConfig) { bd: BookingData =>
withFunctions(
latestOptions = dataService.getLatestServicesByCityIdAndClinicId(userId.userId, bd.cityId.id, bd.clinicId.optionalId),
staticOptions = apiService.getAllServices(userId.userId, bd.cityId.id, bd.clinicId.optionalId),
applyId = id => bd.copy(serviceId = id))
}(requestNext = RequestDoctor)
requestStaticData(RequestDoctor, AwaitDoctor, doctorConfig) { bd: BookingData =>
withFunctions(
latestOptions = dataService.getLatestDoctorsByCityIdAndClinicIdAndServiceId(userId.userId, bd.cityId.id, bd.clinicId.optionalId, bd.serviceId.id),
staticOptions = apiService.getAllDoctors(userId.userId, bd.cityId.id, bd.clinicId.optionalId, bd.serviceId.id),
applyId = id => bd.copy(doctorId = id))
}(requestNext = RequestDateFrom)
whenSafe(RequestDateFrom) {
case Event(_, bookingData: BookingData) =>
datePicker ! DateFromMode
datePicker ! bookingData.dateFrom
goto(AwaitDateFrom)
}
whenSafe(AwaitDateFrom) {
case Event(cmd: Command, _) =>
datePicker ! cmd
stay()
case Event(date: ZonedDateTime, bookingData: BookingData) =>
invokeNext()
goto(RequestDateTo) using bookingData.copy(dateFrom = date)
}
whenSafe(RequestDateTo) {
case Event(_, bookingData: BookingData) =>
datePicker ! DateToMode
datePicker ! bookingData.dateFrom.plusDays(1)
goto(AwaitDateTo)
}
whenSafe(AwaitDateTo) {
case Event(cmd: Command, _) =>
datePicker ! cmd
stay()
case Event(date: ZonedDateTime, bookingData: BookingData) =>
invokeNext()
goto(RequestDayTime) using bookingData.copy(dateTo = date)
}
whenSafe(RequestDayTime) {
case Event(Next, _: BookingData) =>
bot.sendMessage(userId.source, lang.chooseTimeOfDay,
inlineKeyboard = createInlineKeyboard(lang.timeOfDay.map { case (id, label) => Button(label, id.toString) }.toSeq, columns = 1))
goto(AwaitDayTime)
}
whenSafe(AwaitDayTime) {
case Event(Command(_, msg, Some(timeIdStr)), bookingData: BookingData) =>
invokeNext()
val timeId = timeIdStr.toInt
bot.sendEditMessage(userId.source, msg.messageId, lang.preferredTimeIs(timeId))
goto(RequestAction) using bookingData.copy(timeOfDay = timeId)
}
whenSafe(RequestAction) {
case Event(Next, bookingData: BookingData) =>
dataService.storeAppointment(userId.userId, bookingData)
bot.sendMessage(userId.source,
lang.bookingSummary(bookingData),
inlineKeyboard = createInlineKeyboard(Seq(Button(lang.findTerms, Tags.FindTerms), Button(lang.modifyDate, Tags.ModifyDate))))
goto(AwaitAction)
}
whenSafe(AwaitAction) {
case Event(Command(_, _, Some(Tags.FindTerms)), _) =>
invokeNext()
goto(RequestTerm)
case Event(Command(_, _, Some(Tags.ModifyDate)), _) =>
invokeNext()
goto(RequestDateFrom)
}
whenSafe(RequestTerm) {
case Event(Next, bookingData: BookingData) =>
val availableTerms = apiService.getAvailableTerms(userId.userId, bookingData.cityId.id,
bookingData.clinicId.optionalId, bookingData.serviceId.id, bookingData.doctorId.optionalId,
bookingData.dateFrom, Some(bookingData.dateTo), timeOfDay = bookingData.timeOfDay)
termsPager ! availableTerms
goto(AwaitTerm)
}
whenSafe(AwaitTerm) {
case Event(Command(_, _, Some(Tags.ModifyDate)), _) =>
invokeNext()
goto(RequestDateFrom)
case Event(Command(_, _, Some(Tags.CreateMonitoring)), _) =>
invokeNext()
goto(AskMonitoringOptions)
case Event(cmd: Command, _) =>
termsPager ! cmd
stay()
case Event(term: AvailableVisitsTermPresentation, _) =>
self ! term
goto(RequestReservation)
case Event(Pager.NoItemsFound, _) =>
bot.sendMessage(userId.source, lang.noTermsFound, inlineKeyboard =
createInlineKeyboard(Seq(Button(lang.modifyDate, Tags.ModifyDate), Button(lang.createMonitoring, Tags.CreateMonitoring))))
stay()
}
whenSafe(RequestReservation) {
case Event(term: AvailableVisitsTermPresentation, bookingData: BookingData) =>
val response = apiService.temporaryReservation(userId.userId, term.mapTo[TemporaryReservationRequest], term.mapTo[ValuationsRequest])
response match {
case Left(ex) =>
bot.sendMessage(userId.source, ex.getMessage)
invokeNext()
stay()
case Right((temporaryReservation, valuations)) =>
bot.sendMessage(userId.source, lang.confirmAppointment(term, valuations),
inlineKeyboard = createInlineKeyboard(Seq(Button(lang.cancel, Tags.Cancel), Button(lang.book, Tags.Book))))
goto(AwaitReservation) using bookingData.copy(term = Some(term), temporaryReservationId = Some(temporaryReservation.id), valuations = Some(valuations))
}
}
whenSafe(AwaitReservation) {
case Event(Command(_, _, Some(Tags.Cancel)), bookingData: BookingData) =>
apiService.deleteTemporaryReservation(userId.userId, bookingData.temporaryReservationId.get)
stay()
case Event(Command(_, _, Some(Tags.Book)), bookingData: BookingData) =>
val reservationRequestMaybe = for {
tmpReservationId <- bookingData.temporaryReservationId
valuations <- bookingData.valuations
visitTermVariant <- valuations.visitTermVariants.headOption
term <- bookingData.term
} yield (tmpReservationId, visitTermVariant, term).mapTo[ReservationRequest]
reservationRequestMaybe match {
case Some(reservationRequest) =>
apiService.reservation(userId.userId, reservationRequest) match {
case Left(ex) =>
bot.sendMessage(userId.source, ex.getMessage)
invokeNext()
stay()
case Right(success) =>
log.debug(s"Successfully confirmed: $success")
bot.sendMessage(userId.source, lang.appointmentIsConfirmed)
stay()
}
}
}
whenSafe(AskMonitoringOptions) {
case Event(Next, _) =>
bot.sendMessage(userId.source, lang.chooseTypeOfMonitoring,
inlineKeyboard = createInlineKeyboard(Seq(Button(lang.bookByApplication, Tags.BookByApplication), Button(lang.bookManually, Tags.BookManually)), columns = 1))
stay()
case Event(Command(_, _, Some(autobookStr)), bookingData: BookingData) =>
val autobook = autobookStr.toBoolean
invokeNext()
goto(CreateMonitoring) using bookingData.copy(autobook = autobook)
}
whenSafe(CreateMonitoring) {
case Event(Next, bookingData: BookingData) =>
LOG.debug(s"Creating monitoring for $bookingData")
Try(monitoringService.createMonitoring((userId -> bookingData).mapTo[Monitoring])) match {
case Success(_) => bot.sendMessage(userId.source, lang.monitoringHasBeenCreated)
case Failure(ex) =>
LOG.error("Unable to create monitoring", ex)
bot.sendMessage(userId.source, lang.unableToCreateMonitoring)
}
goto(RequestCity) using BookingData()
}
whenUnhandledSafe {
case Event(Init, _) =>
reinit()
case e: Event =>
LOG.error(s"Unhandled event in state:$stateName. Event: $e")
stay()
}
private def cityConfig = StaticDataConfig(lang.city, "Wrocław", isAnyAllowed = false)
private def clinicConfig = StaticDataConfig(lang.clinic, "Swobodna 1", isAnyAllowed = true)
private def serviceConfig = StaticDataConfig(lang.service, "Stomatolog", isAnyAllowed = false)
private def doctorConfig = StaticDataConfig(lang.doctor, "Bartniak", isAnyAllowed = true)
private def reinit() = {
invokeNext()
datePicker ! Init
staticData ! Init
termsPager ! Init
goto(RequestCity) using BookingData()
}
initialize()
override def postStop(): Unit = {
datePicker ! PoisonPill
staticData ! PoisonPill
termsPager ! PoisonPill
super.postStop()
}
}
object Book {
def props(userId: UserId, bot: Bot, apiService: ApiService, dataService: DataService, monitoringService: MonitoringService,
localization: Localization, datePickerActorFactory: (UserId, ActorRef) => ActorRef,
staticDataActorFactory: (UserId, ActorRef) => ActorRef, termsPagerActorFactory: (UserId, ActorRef) => ActorRef): Props =
Props(classOf[Book], userId, bot, apiService, dataService, monitoringService, localization, datePickerActorFactory,
staticDataActorFactory, termsPagerActorFactory)
object RequestCity extends FSMState
object AwaitCity extends FSMState
object RequestClinic extends FSMState
object AwaitClinic extends FSMState
object RequestService extends FSMState
object AwaitService extends FSMState
object RequestDoctor extends FSMState
object AwaitDoctor extends FSMState
object CreateMonitoring extends FSMState
object AskMonitoringOptions extends FSMState
object RequestDateFrom extends FSMState
object AwaitDateFrom extends FSMState
object RequestDateTo extends FSMState
object AwaitDateTo extends FSMState
object RequestDayTime extends FSMState
object AwaitDayTime extends FSMState
object RequestAction extends FSMState
object AwaitAction extends FSMState
object RequestTerm extends FSMState
object AwaitTerm extends FSMState
object RequestReservation extends FSMState
object AwaitReservation extends FSMState
case class BookingData(cityId: IdName = null, clinicId: IdName = null,
serviceId: IdName = null, doctorId: IdName = null, dateFrom: ZonedDateTime = ZonedDateTime.now(),
dateTo: ZonedDateTime = ZonedDateTime.now().plusDays(1L), timeOfDay: Int = 0, autobook: Boolean = false, term: Option[AvailableVisitsTermPresentation] = None,
temporaryReservationId: Option[Long] = None, valuations: Option[ValuationsResponse] = None) extends FSMData
object Tags {
val Cancel = "cancel"
val Book = "book"
val FindTerms = "find_terms"
val ModifyDate = "modify_date"
val CreateMonitoring = "create_monitoring"
val BookManually = "false"
val BookByApplication = "true"
}
}

View File

@@ -0,0 +1,135 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.actor
import akka.actor.{ActorRef, PoisonPill, Props}
import com.lbs.bot.model.{Button, Command}
import com.lbs.bot.{Bot, _}
import com.lbs.server.actor.Bug._
import com.lbs.server.actor.Chat.Init
import com.lbs.server.actor.Login.UserId
import com.lbs.server.lang.{Localizable, Localization}
import com.lbs.server.repository.model
import com.lbs.server.service.DataService
import com.lbs.server.util.MessageExtractors
class Bug(val userId: UserId, bot: Bot, dataService: DataService, bugPagerActorFactory: (UserId, ActorRef) => ActorRef,
val localization: Localization) extends SafeFSM[FSMState, FSMData] with Localizable {
private val bugPager = bugPagerActorFactory(userId, self)
startWith(RequestAction, null)
whenSafe(RequestAction) {
case Event(Next, _) =>
bot.sendMessage(userId.source, lang.bugAction, inlineKeyboard =
createInlineKeyboard(Seq(Button(lang.createNewBug, Tags.SubmitNew), Button(lang.showSubmittedBugs, Tags.ListSubmitted))))
goto(AwaitAction)
}
whenSafe(AwaitAction) {
case Event(Command(_, _, Some(Tags.SubmitNew)), _) =>
bot.sendMessage(userId.source, lang.enterIssueDetails)
goto(AwaitBugDescription)
case Event(Command(_, _, Some(Tags.ListSubmitted)), _) =>
invokeNext()
goto(RequestData)
}
whenSafe(RequestData) {
case Event(Next, _) =>
val bugs = dataService.getBugs(userId.userId)
bugPager ! Init
bugPager ! Right[Throwable, Seq[model.Bug]](bugs)
goto(AwaitPage)
}
whenSafe(AwaitPage) {
case Event(cmd: Command, _) =>
bugPager ! cmd
stay()
case Event(Pager.NoItemsFound, _) =>
bot.sendMessage(userId.source, lang.noSubmittedIssuesFound)
goto(RequestData)
}
whenSafe(AwaitBugDescription) {
case Event(Command(_, MessageExtractors.Text(details), _), _) =>
val bugId = dataService.submitBug(userId.userId, userId.source.sourceSystem.id, details)
bot.sendMessage(userId.source, lang.bugHasBeenCreated(bugId.getOrElse(-1L)))
goto(RequestAction) using null
}
whenUnhandledSafe {
case Event(Init, _) =>
invokeNext()
bugPager ! Init
goto(RequestAction)
}
initialize()
override def postStop(): Unit = {
bugPager ! PoisonPill
super.postStop()
}
}
object Bug {
def props(userId: UserId, bot: Bot, dataService: DataService, bugPagerActorFactory: (UserId, ActorRef) => ActorRef, localization: Localization): Props =
Props(classOf[Bug], userId, bot, dataService, bugPagerActorFactory, localization)
object RequestBugDetails extends FSMState
object AwaitBugDescription extends FSMState
object RequestAction extends FSMState
object AwaitAction extends FSMState
object RequestData extends FSMState
object AwaitPage extends FSMState
object Tags {
val SubmitNew = "submit"
val ListSubmitted = "list"
}
}

View File

@@ -0,0 +1,196 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.actor
import akka.actor.{ActorRef, PoisonPill, Props}
import com.lbs.bot.model.Command
import com.lbs.common.Logger
import com.lbs.server.actor.Chat._
import com.lbs.server.actor.Login.UserId
import com.lbs.server.service.{DataService, MonitoringService}
import com.lbs.server.util.MessageExtractors._
import scala.util.matching.Regex
class Chat(val userId: UserId, dataService: DataService, monitoringService: MonitoringService, bookingActorFactory: UserId => ActorRef, helpActorFactory: UserId => ActorRef,
monitoringsActorFactory: UserId => ActorRef, historyActorFactory: UserId => ActorRef,
visitsActorFactory: UserId => ActorRef, settingsActorFactory: UserId => ActorRef,
bugActorFactory: UserId => ActorRef) extends SafeFSM[FSMState, FSMData] with Logger {
private val bookingActor = bookingActorFactory(userId)
private val helpActor = helpActorFactory(userId)
private val monitoringsActor = monitoringsActorFactory(userId)
private val historyActor = historyActorFactory(userId)
private val visitsActor = visitsActorFactory(userId)
private val settingsActor = settingsActorFactory(userId)
private val bugActor = bugActorFactory(userId)
startWith(HelpChat, null)
when(HelpChat, helpActor) {
case Event(cmd@Command(_, Text("/help"), _), _) =>
helpActor ! cmd
stay()
case Event(cmd@Command(_, Text("/start"), _), _) =>
helpActor ! cmd
stay()
}
when(BookChat, bookingActor) {
case Event(Command(_, Text("/book"), _), _) =>
bookingActor ! Init
stay()
}
when(HistoryChat, historyActor) {
case Event(Command(_, Text("/history"), _), _) =>
historyActor ! Init
stay()
}
when(VisitsChat, visitsActor) {
case Event(Command(_, Text("/visits"), _), _) =>
visitsActor ! Init
stay()
}
when(BugChat, bugActor) {
case Event(Command(_, Text("/bug"), _), _) =>
bugActor ! Init
goto(BugChat)
}
when(MonitoringsChat, monitoringsActor) {
case Event(Command(_, Text("/monitorings"), _), _) =>
monitoringsActor ! Init
stay()
}
when(SettingsChat, settingsActor) {
case Event(Command(_, Text("/settings"), _), _) =>
settingsActor ! Init
stay()
}
private def when(state: FSMState, actor: ActorRef)(mainStateFunction: StateFunction): Unit = {
whenSafe(state) {
case event: Event =>
if (mainStateFunction.isDefinedAt(event)) mainStateFunction(event)
else {
val secondaryStateFunction = secondaryState(actor)
if (secondaryStateFunction.isDefinedAt(event)) secondaryStateFunction(event)
else eventHandler(event)
}
}
}
private def secondaryState(actor: ActorRef): StateFunction = {
case Event(cmd@Command(_, Text("/bug"), _), _) =>
self ! cmd
goto(BugChat)
case Event(cmd@Command(_, Text("/help"), _), _) =>
self ! cmd
goto(HelpChat)
case Event(cmd@Command(_, Text("/start"), _), _) =>
self ! cmd
goto(HelpChat)
case Event(cmd@Command(_, Text("/book"), _), _) =>
self ! cmd
goto(BookChat)
case Event(cmd@Command(_, Text("/monitorings"), _), _) =>
self ! cmd
goto(MonitoringsChat)
case Event(cmd@Command(_, Text("/history"), _), _) =>
self ! cmd
goto(HistoryChat)
case Event(cmd@Command(_, Text("/visits"), _), _) =>
self ! cmd
goto(VisitsChat)
case Event(cmd@Command(_, Text("/settings"), _), _) =>
self ! cmd
goto(SettingsChat)
case Event(cmd@Command(_, Text(MonitoringId(monitoringIdStr, scheduleIdStr, timeStr)), _), _) =>
val monitoringId = monitoringIdStr.toLong
val scheduleId = scheduleIdStr.toLong
val time = timeStr.toLong
monitoringService.bookAppointmentByScheduleId(userId.userId, monitoringId, scheduleId, time)
stay()
case Event(cmd: Command, _) =>
actor ! cmd
stay()
}
whenUnhandledSafe {
case e: Event =>
LOG.debug(s"Unhandled event in state:$stateName. Event: $e")
stay()
}
initialize()
override def postStop(): Unit = {
bookingActor ! PoisonPill
helpActor ! PoisonPill
monitoringsActor ! PoisonPill
historyActor ! PoisonPill
visitsActor ! PoisonPill
settingsActor ! PoisonPill
bugActor ! PoisonPill
super.postStop()
}
}
object Chat {
def props(userId: UserId, dataService: DataService, monitoringService: MonitoringService, bookingActorFactory: UserId => ActorRef, helpActorFactory: UserId => ActorRef,
monitoringsActorFactory: UserId => ActorRef, historyActorFactory: UserId => ActorRef,
visitsActorFactory: UserId => ActorRef, settingsActorFactory: UserId => ActorRef, bugActorFactory: UserId => ActorRef): Props =
Props(classOf[Chat], userId, dataService, monitoringService, bookingActorFactory, helpActorFactory, monitoringsActorFactory,
historyActorFactory, visitsActorFactory, settingsActorFactory, bugActorFactory)
object HelpChat extends FSMState
object BookChat extends FSMState
object MonitoringsChat extends FSMState
object HistoryChat extends FSMState
object VisitsChat extends FSMState
object SettingsChat extends FSMState
object BugChat extends FSMState
object Init
val MonitoringId: Regex = s"/reserve_(\\d+)_(\\d+)_(\\d+)".r
}

View File

@@ -0,0 +1,155 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.actor
import java.time.format.TextStyle
import java.time.{LocalTime, ZonedDateTime}
import akka.actor.{ActorRef, Props}
import com.lbs.bot.model.{Button, Command}
import com.lbs.bot.{Bot, _}
import com.lbs.server.actor.Chat.Init
import com.lbs.server.actor.DatePicker._
import com.lbs.server.actor.Login.UserId
import com.lbs.server.lang.{Localizable, Localization}
/**
* Date picker Inline Keyboard
*
* ⬆ ⬆ ⬆
* dd MM yyyy
* ⬇ ⬇ ⬇
*
*/
class DatePicker(val userId: UserId, val bot: Bot, val localization: Localization, originator: ActorRef) extends SafeFSM[FSMState, ZonedDateTime] with Localizable {
startWith(AwaitMode, null)
private var mode: Mode = DateFromMode
whenSafe(AwaitMode) {
case Event(newMode: Mode, _) =>
mode = newMode
goto(RequestDate)
}
whenSafe(RequestDate) {
case Event(initialDate: ZonedDateTime, _) =>
val message = mode match {
case DateFromMode => lang.chooseDateFrom
case DateToMode => lang.chooseDateTo
}
bot.sendMessage(userId.source, message, inlineKeyboard = dateButtons(initialDate))
goto(AwaitDate) using initialDate
}
whenSafe(AwaitDate) {
case Event(Command(_, msg, Some(Tags.Done)), finalDate: ZonedDateTime) =>
val (message, updatedDate) = mode match {
case DateFromMode =>
val startOfTheDay = finalDate.`with`(LocalTime.MIN)
val dateFrom = if (startOfTheDay.isBefore(ZonedDateTime.now())) finalDate else startOfTheDay
lang.dateFromIs(dateFrom) -> dateFrom
case DateToMode =>
val dateTo = finalDate.`with`(LocalTime.MAX).minusHours(2)
lang.dateToIs(dateTo) -> dateTo
}
bot.sendEditMessage(userId.source, msg.messageId, message)
originator ! updatedDate
goto(AwaitMode) using null
case Event(Command(_, msg, Some(tag)), date: ZonedDateTime) =>
val modifiedDate = modifyDate(date, tag)
bot.sendEditMessage(userId.source, msg.messageId, inlineKeyboard = dateButtons(modifiedDate))
stay() using modifiedDate
}
whenUnhandledSafe {
case Event(Init, _) =>
goto(AwaitMode) using null
case e: Event =>
LOG.error(s"Unhandled event in state:$stateName. Event: $e")
stay()
}
initialize()
private def modifyDate(date: ZonedDateTime, tag: String) = {
val dateModifier = tag match {
case Tags.DayUp => date.plusDays _
case Tags.MonthUp => date.plusMonths _
case Tags.YearUp => date.plusYears _
case Tags.DayDown => date.minusDays _
case Tags.MonthDown => date.minusMonths _
case Tags.YearDown => date.minusYears _
}
dateModifier(1)
}
private def dateButtons(date: ZonedDateTime) = {
val day = date.getDayOfMonth.toString
val dayOfWeek = date.getDayOfWeek.getDisplayName(TextStyle.SHORT, lang.locale)
val month = date.getMonth.getDisplayName(TextStyle.SHORT, lang.locale)
val year = date.getYear.toString
createInlineKeyboard(Seq(
Seq(Button("⬆", Tags.DayUp), Button("⬆", Tags.MonthUp), Button("⬆", Tags.YearUp)),
Seq(Button(s"$day ($dayOfWeek)"), Button(month), Button(year)),
Seq(Button("⬇", Tags.DayDown), Button("⬇", Tags.MonthDown), Button("⬇", Tags.YearDown)),
Seq(Button("Done", Tags.Done))
))
}
}
object DatePicker {
def props(userId: UserId, bot: Bot, localization: Localization, originator: ActorRef): Props =
Props(classOf[DatePicker], userId, bot, localization, originator)
object RequestDate extends FSMState
object AwaitDate extends FSMState
object AwaitMode extends FSMState
trait Mode
object DateFromMode extends Mode
object DateToMode extends Mode
object Tags {
val DayUp = "day_up"
val MonthUp = "month_up"
val YearUp = "year_up"
val DayDown = "day_down"
val MonthDown = "month_down"
val YearDown = "year_down"
val Done = "done"
}
}

View File

@@ -0,0 +1,26 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.actor
trait FSMData

View File

@@ -0,0 +1,26 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.actor
trait FSMState

View File

@@ -0,0 +1,50 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.actor
import akka.actor.{Actor, Props}
import com.lbs.bot.Bot
import com.lbs.bot.model.Command
import com.lbs.server.actor.Login.UserId
import com.lbs.server.lang.{Localizable, Localization}
class Help(val userId: UserId, bot: Bot, val localization: Localization) extends Actor with Localizable {
override def receive: Receive = {
case _: Command =>
bot.sendMessage(userId.source, lang.help)
}
}
object Help {
def props(userId: UserId, bot: Bot, localization: Localization): Props = Props(classOf[Help], userId, bot, localization)
}

View File

@@ -0,0 +1,97 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.actor
import akka.actor.{ActorRef, PoisonPill, Props}
import com.lbs.api.json.model.HistoricVisit
import com.lbs.bot.Bot
import com.lbs.bot.model.Command
import com.lbs.server.actor.Chat.Init
import com.lbs.server.actor.History.{AwaitPage, RequestData}
import com.lbs.server.actor.Login.UserId
import com.lbs.server.lang.{Localizable, Localization}
import com.lbs.server.service.ApiService
class History(val userId: UserId, bot: Bot, apiService: ApiService, val localization: Localization, historyPagerActorFactory: (UserId, ActorRef) => ActorRef) extends SafeFSM[FSMState, FSMData] with Localizable {
private val historyPager = historyPagerActorFactory(userId, self)
startWith(RequestData, null)
whenSafe(RequestData) {
case Event(Next, _) =>
val visits = apiService.visitsHistory(userId.userId)
historyPager ! visits
goto(AwaitPage)
}
whenSafe(AwaitPage) {
case Event(cmd: Command, _) =>
historyPager ! cmd
stay()
case Event(Pager.NoItemsFound, _) =>
bot.sendMessage(userId.source, lang.visitsHistoryIsEmpty)
goto(RequestData)
case Event(_: HistoricVisit, _) =>
goto(RequestData) using null
}
whenUnhandledSafe {
case Event(Init, _) =>
invokeNext()
historyPager ! Init
goto(RequestData)
}
initialize()
override def postStop(): Unit = {
historyPager ! PoisonPill
super.postStop()
}
}
object History {
def props(userId: UserId, bot: Bot, apiService: ApiService, localization: Localization, historyPagerActorFactory: (UserId, ActorRef) => ActorRef): Props =
Props(classOf[History], userId, bot, apiService, localization, historyPagerActorFactory)
object RequestData extends FSMState
object AwaitPage extends FSMState
}

View File

@@ -0,0 +1,126 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.actor
import akka.actor.{ActorRef, Props}
import com.lbs.bot.Bot
import com.lbs.bot.model.{Command, MessageSource}
import com.lbs.bot.telegram.TelegramBot
import com.lbs.server.actor.Chat.Init
import com.lbs.server.actor.Login._
import com.lbs.server.lang.{Localizable, Localization}
import com.lbs.server.service.{ApiService, DataService}
import com.lbs.server.util.MessageExtractors
import org.jasypt.util.text.TextEncryptor
class Login(source: MessageSource, bot: Bot, dataService: DataService, apiService: ApiService, textEncryptor: TextEncryptor, val localization: Localization, originator: ActorRef) extends SafeFSM[FSMState, FSMData] with Localizable {
protected var userId: UserId = _
startWith(LogIn, LoginData())
private var forwardCommand: ForwardCommand = _
whenSafe(LogIn) {
case Event(cmd: Command, LoginData(None, None)) =>
forwardCommand = ForwardCommand(cmd)
invokeNext()
goto(RequestUsername)
case Event(_, LoginData(Some(username), Some(password))) =>
val loginResult = apiService.login(username, password)
loginResult match {
case Left(error) =>
bot.sendMessage(source, error.getMessage)
invokeNext()
goto(RequestUsername) using LoginData()
case Right(loggedIn) =>
val credentials = dataService.saveCredentials(source, username, password)
userId = UserId(credentials.userId, source)
apiService.addSession(credentials.userId, loggedIn.accessToken, loggedIn.tokenType)
bot.sendMessage(source, lang.loginAndPasswordAreOk)
originator ! LoggedIn(forwardCommand, credentials.userId)
stay() using null
}
}
whenSafe(RequestUsername) {
case Event(Next, _) =>
bot.sendMessage(source, lang.provideUsername)
goto(AwaitUsername)
}
whenSafe(AwaitUsername) {
case Event(Command(_, MessageExtractors.TextOpt(username), _), loginData: LoginData) =>
invokeNext()
goto(RequestPassword) using loginData.copy(username = username)
}
whenSafe(RequestPassword) {
case Event(Next, _) =>
bot.sendMessage(source, lang.providePassword)
goto(AwaitPassword)
}
whenSafe(AwaitPassword) {
case Event(Command(_, MessageExtractors.TextOpt(password), _), loginData: LoginData) =>
invokeNext()
goto(LogIn) using loginData.copy(password = password.map(textEncryptor.encrypt))
}
whenUnhandledSafe {
case Event(Init, _) =>
goto(LogIn) using LoginData()
case e: Event =>
LOG.error(s"Unhandled event in state:$stateName. Event: $e")
stay()
}
initialize()
}
object Login {
def props(source: MessageSource, bot: Bot, dataService: DataService, apiService: ApiService, textEncryptor: TextEncryptor, localization: Localization, originator: ActorRef): Props =
Props(classOf[Login], source, bot, dataService, apiService, textEncryptor, localization, originator)
object LogIn extends FSMState
object RequestUsername extends FSMState
object AwaitUsername extends FSMState
object RequestPassword extends FSMState
object AwaitPassword extends FSMState
case class LoginData(username: Option[String] = None, password: Option[String] = None) extends FSMData
case class ForwardCommand(cmd: Command)
case class UserId(userId: Long, source: MessageSource)
case class LoggedIn(forwardCommand: ForwardCommand, userId: Long)
}

View File

@@ -0,0 +1,113 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.actor
import akka.actor.{ActorRef, PoisonPill, Props}
import com.lbs.bot._
import com.lbs.bot.model.{Button, Command}
import com.lbs.server.actor.Chat.Init
import com.lbs.server.actor.Login.UserId
import com.lbs.server.actor.Monitorings.{AwaitDecision, AwaitPage, RequestData, Tags}
import com.lbs.server.lang.{Localizable, Localization}
import com.lbs.server.repository.model.Monitoring
import com.lbs.server.service.MonitoringService
class Monitorings(val userId: UserId, bot: Bot, monitoringService: MonitoringService, val localization: Localization, monitoringsPagerActorFactory: (UserId, ActorRef) => ActorRef) extends SafeFSM[FSMState, Monitoring] with Localizable {
private val monitoringsPager = monitoringsPagerActorFactory(userId, self)
startWith(RequestData, null)
whenSafe(RequestData) {
case Event(Next, _) =>
val monitorings = monitoringService.getActiveMonitorings(userId.userId)
monitoringsPager ! Right[Throwable, Seq[Monitoring]](monitorings)
goto(AwaitPage)
}
whenSafe(AwaitPage) {
case Event(cmd: Command, _) =>
monitoringsPager ! cmd
stay()
case Event(Pager.NoItemsFound, _) =>
bot.sendMessage(userId.source, lang.noActiveMonitorings)
goto(RequestData)
case Event(monitoring: Monitoring, _) =>
bot.sendMessage(userId.source, lang.deactivateMonitoring(monitoring), inlineKeyboard =
createInlineKeyboard(Seq(Button(lang.no, Tags.No), Button(lang.yes, Tags.Yes))))
goto(AwaitDecision) using monitoring
}
whenSafe(AwaitDecision) {
case Event(Command(_, _, Some(Tags.No)), _) =>
bot.sendMessage(userId.source, lang.monitoringWasNotDeactivated)
goto(RequestData)
case Event(Command(_, _, Some(Tags.Yes)), monitoring: Monitoring) =>
monitoringService.deactivateMonitoring(monitoring.recordId)
bot.sendMessage(userId.source, lang.deactivated)
goto(RequestData)
}
whenUnhandledSafe {
case Event(Init, _) =>
invokeNext()
monitoringsPager ! Init
goto(RequestData)
}
initialize()
override def postStop(): Unit = {
monitoringsPager ! PoisonPill
super.postStop()
}
}
object Monitorings {
def props(userId: UserId, bot: Bot, monitoringService: MonitoringService, localization: Localization, monitoringsPagerActorFactory: (UserId, ActorRef) => ActorRef): Props =
Props(classOf[Monitorings], userId, bot, monitoringService, localization, monitoringsPagerActorFactory)
object RequestData extends FSMState
object AwaitPage extends FSMState
object AwaitDecision extends FSMState
object Tags {
val Yes = "yes"
val No = "no"
}
}

View File

@@ -0,0 +1,132 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.actor
import akka.actor.{ActorRef, Props}
import com.lbs.bot.model.{Button, Command}
import com.lbs.bot.{Bot, _}
import com.lbs.common.Logger
import com.lbs.server.actor.Chat.Init
import com.lbs.server.actor.Login.UserId
import com.lbs.server.actor.Pager.{Tags, _}
import com.lbs.server.lang.{Localizable, Localization}
import com.lbs.server.util.MessageExtractors
class Pager[Data](val userId: UserId, bot: Bot, makeMessage: (Data, Int, Int) => String,
makeHeader: (Int, Int) => String, selectionPrefix: Option[String],
val localization: Localization, originator: ActorRef)
extends SafeFSM[FSMState, FSMData] with Localizable with Logger {
private case class Page(page: Int, pages: Seq[Seq[Data]]) extends FSMData
private val Selection = s"/${selectionPrefix.getOrElse("")}_(\\d+)_(\\d+)".r
startWith(PrepareData, null)
whenSafe(PrepareData) {
case Event(Left(error: Throwable), _) =>
bot.sendMessage(userId.source, error.getMessage)
invokeNext()
goto(PrepareData)
case Event(Right(items: Seq[Data]), _) if items.isEmpty =>
originator ! NoItemsFound
goto(PrepareData) using null
case Event(Right(items: Seq[Data]), _) =>
invokeNext()
goto(RequestData) using Page(0, items.grouped(Pager.PageSize).toList)
}
whenSafe(RequestData) {
case Event(Next, page: Page) =>
sendPage(page.page, page.pages)
goto(AwaitData)
}
whenSafe(AwaitData) {
case Event(Command(_, msg, Some(Tags.Next)), termsData: Page) =>
val page = termsData.page + 1
sendPage(page, termsData.pages, Some(msg.messageId))
stay() using termsData.copy(page = page)
case Event(Command(_, msg, Some(Tags.Previous)), termsData: Page) =>
val page = termsData.page - 1
sendPage(page, termsData.pages, Some(msg.messageId))
stay() using termsData.copy(page = page)
case Event(Command(_, MessageExtractors.Text(Selection(pageStr, indexStr)), _), termsData: Page) if selectionPrefix.nonEmpty =>
val page = pageStr.toInt
val index = indexStr.toInt
originator ! termsData.pages(page)(index)
goto(PrepareData) using null
}
private def sendPage(page: Int, data: Seq[Seq[Data]], messageId: Option[String] = None): Unit = {
val pages = data.length
val message = makeHeader(page, data.length) + "\n\n" + data(page).zipWithIndex.map { case (d, index) => makeMessage(d, page, index) }.mkString
val previousButton = if (page > 0) Some(Button(lang.previous, Tags.Previous)) else None
val nextButton = if (page >= 0 && page < pages - 1) Some(Button(lang.next, Tags.Next)) else None
val buttons = previousButton.toSeq ++ nextButton.toSeq
messageId match {
case Some(id) =>
bot.sendEditMessage(userId.source, id, message, inlineKeyboard = createInlineKeyboard(buttons))
case None =>
bot.sendMessage(userId.source, message, inlineKeyboard = createInlineKeyboard(buttons))
}
}
whenUnhandledSafe {
case Event(Init, _) =>
goto(PrepareData) using null
case e: Event =>
LOG.error(s"Unhandled event in state:$stateName. Event: $e")
stay()
}
initialize()
}
object Pager {
def props[Data](userId: UserId, bot: Bot,
makeMessage: (Data, Int, Int) => String, makeHeader: (Int, Int) => String, dataPrefix: Option[String], localization: Localization, originator: ActorRef): Props =
Props(classOf[Pager[Data]], userId, bot, makeMessage, makeHeader, dataPrefix, localization, originator)
val PageSize = 5
object PrepareData extends FSMState
object RequestData extends FSMState
object AwaitData extends FSMState
object NoItemsFound
object Tags {
val Previous = "previous"
val Next = "next"
}
}

View File

@@ -0,0 +1,70 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.actor
import akka.actor.{ActorRef, Props}
import com.lbs.api.json.model.{AvailableVisitsTermPresentation, HistoricVisit, ReservedVisit}
import com.lbs.bot.Bot
import com.lbs.server.actor.Login.UserId
import com.lbs.server.lang.{Localizable, Localization}
import com.lbs.server.repository.model
import com.lbs.server.repository.model.Monitoring
class Pagers(val userId: UserId, bot: Bot, val localization: Localization) extends Localizable {
def termsPagerProps(originator: ActorRef): Props =
Pager.props[AvailableVisitsTermPresentation](userId, bot,
(term: AvailableVisitsTermPresentation, page: Int, index: Int) => lang.termEntry(term, page, index),
(page: Int, pages: Int) => lang.termsHeader(page, pages),
Some("book"), localization, originator)
def historyPagerProps(originator: ActorRef): Props =
Pager.props[HistoricVisit](userId, bot,
(visit: HistoricVisit, page: Int, index: Int) => lang.historyEntry(visit, page, index),
(page: Int, pages: Int) => lang.historyHeader(page, pages),
Some("repeat"), localization, originator)
def visitsPagerProps(originator: ActorRef): Props =
Pager.props[ReservedVisit](userId, bot,
(visit: ReservedVisit, page: Int, index: Int) => lang.upcomingVisitEntry(visit, page, index),
(page: Int, pages: Int) => lang.upcomingVisitsHeader(page, pages),
Some("cancel"), localization, originator)
def bugPagerProps(originator: ActorRef): Props =
Pager.props[model.Bug](userId, bot,
(bug: model.Bug, page: Int, index: Int) => lang.bugEntry(bug, page, index),
(page: Int, pages: Int) => lang.bugsHeader(page, pages),
None, localization, originator)
def monitoringsPagerProps(originator: ActorRef): Props =
Pager.props[Monitoring](userId, bot,
(monitoring: Monitoring, page: Int, index: Int) => lang.monitoringEntry(monitoring, page, index),
(page: Int, pages: Int) => lang.monitoringsHeader(page, pages),
Some("cancel"), localization, originator)
}
object Pagers {
def apply(userId: UserId, bot: Bot, localization: Localization) = new Pagers(userId, bot, localization)
}

View File

@@ -0,0 +1,85 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.actor
import akka.actor.{Actor, ActorRef, Cancellable, PoisonPill, Props}
import com.lbs.bot.model.{Command, MessageSource}
import com.lbs.common.Logger
import com.lbs.server.actor.Router.DestroyChat
import scala.collection.mutable
import scala.concurrent.ExecutionContextExecutor
import scala.concurrent.duration.DurationLong
class Router(authActorFactory: MessageSource => ActorRef) extends Actor with Logger {
private val chats = mutable.Map.empty[MessageSource, ActorRef]
private val timers = mutable.Map.empty[MessageSource, Cancellable]
private val idleTimeout = 1.hour
private implicit val dispatcher: ExecutionContextExecutor = context.system.dispatcher
override def receive: Receive = {
case cmd@Command(source, _, _) =>
scheduleIdleChatDestroyer(source)
val chat = chats.get(source) match {
case Some(actor) => actor
case None =>
val actor = authActorFactory(source)
chats += source -> actor
actor
}
chat ! cmd
case DestroyChat(source) =>
destroyChat(source)
case what => LOG.info(s"Unknown message: $what")
}
private def destroyChat(source: MessageSource): Unit = {
LOG.info(s"Destroying chat for $source due to $idleTimeout inactivity")
timers.remove(source)
chats.remove(source).foreach(_ ! PoisonPill)
}
private def scheduleIdleChatDestroyer(source: MessageSource): Unit = {
timers.remove(source).foreach(_.cancel())
val cancellable = context.system.scheduler.scheduleOnce(idleTimeout) {
self ! DestroyChat(source)
}
timers += source -> cancellable
}
override def postStop(): Unit = {
chats.foreach(_._2 ! PoisonPill)
}
}
object Router {
def props(authActorFactory: MessageSource => ActorRef) = Props(classOf[Router], authActorFactory)
case class DestroyChat(source: MessageSource)
}

View File

@@ -0,0 +1,57 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.actor
import akka.actor.FSM
import com.lbs.common.Logger
trait SafeFSM[S, D] extends FSM[S, D] with Logger {
protected val defaultEventHandler: StateFunction = {
case e: Event =>
LOG.warn(s"Unhandled event in state:$stateName. Event: $e")
stay()
}
protected var eventHandler: StateFunction = defaultEventHandler
protected def whenSafe(state: S)(stateFunction: StateFunction): Unit = {
when(state) {
case event: Event =>
try {
if (stateFunction.isDefinedAt(event)) stateFunction(event)
else eventHandler(event)
} catch {
case e: Exception =>
LOG.error(s"Exception occurred while processing event $event", e)
stay()
}
}
}
protected def whenUnhandledSafe(stateFunction: StateFunction): Unit = {
whenUnhandled(stateFunction)
eventHandler = stateFunction orElse defaultEventHandler
}
}

View File

@@ -0,0 +1,103 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.actor
import akka.actor.Props
import com.lbs.bot.model.{Button, Command}
import com.lbs.bot.{Bot, _}
import com.lbs.server.actor.Chat.Init
import com.lbs.server.actor.Login.UserId
import com.lbs.server.actor.Settings._
import com.lbs.server.lang.{Lang, Localizable, Localization}
import com.lbs.server.service.DataService
class Settings(val userId: UserId, bot: Bot, dataService: DataService, val localization: Localization) extends SafeFSM[FSMState, FSMData] with Localizable {
startWith(RequestAction, null)
whenSafe(RequestAction) {
case Event(Next, _) =>
bot.sendMessage(userId.source, lang.settingsHeader, inlineKeyboard =
createInlineKeyboard(Seq(Button(lang.language, Tags.Language))))
goto(AwaitAction)
}
whenSafe(AwaitAction) {
case Event(Command(_, _, Some(Tags.Language)), _) =>
bot.sendMessage(userId.source, lang.chooseLanguage,
inlineKeyboard = createInlineKeyboard(Lang.Langs.map(l => Button(l.label, l.id)), columns = 1))
goto(AwaitLanguage)
}
whenSafe(AwaitLanguage) {
case Event(Command(_, _, Some(langIdStr)), _) =>
val langId = langIdStr.toInt
localization.updateLanguage(userId.userId, Lang(langId))
bot.sendMessage(userId.source, lang.languageUpdated)
goto(RequestAction) using null
}
whenUnhandledSafe {
case Event(Init, _) =>
invokeNext()
goto(RequestAction)
}
initialize()
}
object Settings {
def props(userId: UserId, bot: Bot, dataService: DataService, localization: Localization): Props =
Props(classOf[Settings], userId, bot, dataService, localization)
object AwaitLanguage extends FSMState
object RequestAction extends FSMState
object AwaitAction extends FSMState
object Tags {
val Language = "language"
}
}

View File

@@ -0,0 +1,133 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.actor
import akka.actor.{ActorRef, Props}
import com.lbs.api.json.model.IdName
import com.lbs.bot.model.{Button, Command, TaggedButton}
import com.lbs.bot.{Bot, _}
import com.lbs.server.actor.Chat.Init
import com.lbs.server.actor.Login.UserId
import com.lbs.server.actor.StaticData._
import com.lbs.server.lang.{Localizable, Localization}
class StaticData(val userId: UserId, bot: Bot, val localization: Localization, originator: ActorRef) extends SafeFSM[FSMState, IdName] with Localizable {
private def anySelectOption: List[TaggedButton] = if (config.isAnyAllowed) List(Button(lang.any, -1L)) else List()
startWith(AwaitConfig, null)
private var config: StaticDataConfig = _
private var callbackTags: List[TaggedButton] = List()
whenSafe(AwaitConfig) {
case Event(newConfig: StaticDataConfig, _) =>
config = newConfig
invokeNext()
goto(RequestStaticData)
}
whenSafe(RequestStaticData) {
case Event(Next, _) =>
originator ! LatestOptions
stay()
case Event(LatestOptions(options), _) if options.isEmpty =>
callbackTags = anySelectOption
bot.sendMessage(userId.source, lang.pleaseEnterStaticDataNameOrAny(config),
inlineKeyboard = createInlineKeyboard(callbackTags))
goto(AwaitStaticData)
case Event(LatestOptions(options), _) if options.nonEmpty =>
callbackTags = anySelectOption ++ options.map(data => Button(data.name, data.id))
bot.sendMessage(userId.source, lang.pleaseEnterStaticDataNameOrPrevious(config),
inlineKeyboard = createInlineKeyboard(callbackTags, columns = 1))
goto(AwaitStaticData)
}
whenSafe(AwaitStaticData) {
case Event(Command(_, msg, Some(tag)), _) =>
val id = tag.toLong
val label = callbackTags.find(_.tag == tag).map(_.label).getOrElse(sys.error("Unable to get callback tag label"))
bot.sendEditMessage(userId.source, msg.messageId, lang.staticDataIs(config, label))
originator ! IdName(id, label)
goto(AwaitConfig)
case Event(Command(_, msg, _), _) =>
val searchText = msg.text.get.toLowerCase
originator ! FindOptions(searchText)
stay()
case Event(FoundOptions(Right(options)), _) if options.nonEmpty =>
callbackTags = anySelectOption ::: options.map(c => Button(c.name, c.id))
bot.sendMessage(userId.source, lang.pleaseChooseStaticDataNameOrAny(config),
inlineKeyboard = createInlineKeyboard(callbackTags, columns = 1))
stay()
case Event(FoundOptions(Right(options)), _) if options.isEmpty =>
callbackTags = anySelectOption
bot.sendMessage(userId.source, lang.staticNotFound(config), inlineKeyboard = createInlineKeyboard(callbackTags))
stay()
case Event(FoundOptions(Left(ex)), _) =>
bot.sendMessage(userId.source, ex.getMessage)
stay()
}
whenUnhandledSafe {
case Event(Init, _) =>
goto(AwaitConfig) using null
case e: Event =>
LOG.error(s"Unhandled event in state:$stateName. Event: $e")
stay()
}
initialize()
}
object StaticData {
def props(userId: UserId, bot: Bot, localization: Localization, originator: ActorRef): Props =
Props(classOf[StaticData], userId, bot, localization, originator)
object AwaitConfig extends FSMState
object RequestStaticData extends FSMState
object AwaitStaticData extends FSMState
case class StaticDataConfig(name: String, example: String, isAnyAllowed: Boolean)
object LatestOptions
case class LatestOptions(options: Seq[IdName])
case class FindOptions(searchText: String)
case class FoundOptions(option: Either[Throwable, List[IdName]])
}

View File

@@ -0,0 +1,69 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.actor
import akka.actor.ActorRef
import com.lbs.api.json.model.IdName
import com.lbs.bot.model.Command
import com.lbs.server.actor.Book.BookingData
import com.lbs.server.actor.StaticData.{FindOptions, FoundOptions, LatestOptions, StaticDataConfig}
trait StaticDataForBooking extends SafeFSM[FSMState, FSMData] {
protected def staticData: ActorRef
protected def withFunctions(latestOptions: => Seq[IdName], staticOptions: => Either[Throwable, List[IdName]], applyId: IdName => BookingData): FSMState => StateFunction = {
nextState: FSMState => {
case Event(cmd: Command, _) =>
staticData ! cmd
stay()
case Event(LatestOptions, _) =>
staticData ! LatestOptions(latestOptions)
stay()
case Event(FindOptions(searchText), _) =>
staticData ! FoundOptions(filterOptions(staticOptions, searchText))
stay()
case Event(id: IdName, _) =>
invokeNext()
goto(nextState) using applyId(id)
}
}
protected def requestStaticData(requestState: FSMState, awaitState: FSMState, staticDataConfig: => StaticDataConfig)(functions: BookingData => FSMState => StateFunction)(requestNext: FSMState): Unit = {
whenSafe(requestState) {
case Event(_, _) =>
staticData ! staticDataConfig
goto(awaitState)
}
whenSafe(awaitState) {
case event@Event(_, bookingData: BookingData) =>
val fn = functions(bookingData)(requestNext)
if (fn.isDefinedAt(event)) fn(event) else eventHandler(event)
}
}
private def filterOptions(options: Either[Throwable, List[IdName]], searchText: String) = {
options.map(opt => opt.filter(c => c.name.toLowerCase.contains(searchText)))
}
}

View File

@@ -0,0 +1,50 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.actor
import akka.actor.{Actor, Props}
import com.lbs.bot.Bot
import com.lbs.bot.model.{Command, MessageSource}
import com.lbs.server.lang.En
class UnauthorizedHelp(source: MessageSource, bot: Bot) extends Actor {
override def receive: Receive = {
case _: Command =>
bot.sendMessage(source, En.help)
}
}
object UnauthorizedHelp {
def props(source: MessageSource, bot: Bot): Props = Props(classOf[UnauthorizedHelp], source, bot)
}

View File

@@ -0,0 +1,119 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.actor
import akka.actor.{ActorRef, PoisonPill, Props}
import com.lbs.api.json.model.ReservedVisit
import com.lbs.bot.model.{Button, Command}
import com.lbs.bot.{Bot, _}
import com.lbs.server.actor.Chat.Init
import com.lbs.server.actor.Login.UserId
import com.lbs.server.actor.Visits.{AwaitDecision, AwaitPage, RequestData, Tags}
import com.lbs.server.lang.{Localizable, Localization}
import com.lbs.server.service.ApiService
class Visits(val userId: UserId, bot: Bot, apiService: ApiService, val localization: Localization,
visitsPagerActorFactory: (UserId, ActorRef) => ActorRef) extends SafeFSM[FSMState, ReservedVisit] with Localizable {
private val reservedVisitsPager = visitsPagerActorFactory(userId, self)
startWith(RequestData, null)
whenSafe(RequestData) {
case Event(Next, _) =>
val visits = apiService.reservedVisits(userId.userId)
reservedVisitsPager ! visits
goto(AwaitPage)
}
whenSafe(AwaitPage) {
case Event(cmd: Command, _) =>
reservedVisitsPager ! cmd
stay()
case Event(Pager.NoItemsFound, _) =>
bot.sendMessage(userId.source, lang.noUpcomingVisits)
goto(RequestData)
case Event(visit: ReservedVisit, _) =>
bot.sendMessage(userId.source, lang.areYouSureToCancelAppointment(visit),
inlineKeyboard = createInlineKeyboard(Seq(Button(lang.no, Tags.No), Button(lang.yes, Tags.Yes))))
goto(AwaitDecision) using visit
}
whenSafe(AwaitDecision) {
case Event(Command(_, _, Some(Tags.No)), _) =>
bot.sendMessage(userId.source, lang.appointmentWasNotCancelled)
goto(RequestData)
case Event(Command(_, _, Some(Tags.Yes)), visit: ReservedVisit) =>
apiService.deleteReservation(userId.userId, visit.reservationId) match {
case Left(ex) => bot.sendMessage(userId.source, lang.unableToCancelUpcomingVisit(ex.getMessage))
case Right(r) => bot.sendMessage(userId.source, lang.appointmentHasBeenCancelled)
}
goto(RequestData)
}
whenUnhandledSafe {
case Event(Init, _) =>
invokeNext()
reservedVisitsPager ! Init
goto(RequestData)
}
initialize()
override def postStop(): Unit = {
reservedVisitsPager ! PoisonPill
super.postStop()
}
}
object Visits {
def props(userId: UserId, bot: Bot, apiService: ApiService, localization: Localization,
visitsPagerActorFactory: (UserId, ActorRef) => ActorRef): Props =
Props(classOf[Visits], userId, bot, apiService, localization, visitsPagerActorFactory)
object RequestData extends FSMState
object AwaitPage extends FSMState
object AwaitDecision extends FSMState
object Tags {
val Yes = "yes"
val No = "no"
}
}

View File

@@ -0,0 +1,34 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server
import akka.actor.ActorRef
package object actor {
def invokeNext()(implicit self: ActorRef): Unit = {
self ! Next
}
object Next
}

View File

@@ -0,0 +1,26 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.exception
case class UserNotFoundException(chatId: Long) extends Exception(s"Luxmed username for char with id $chatId")

View File

@@ -0,0 +1,351 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.lang
import java.time.ZonedDateTime
import java.util.Locale
import com.lbs.api.json.model.{AvailableVisitsTermPresentation, HistoricVisit, ReservedVisit, ValuationsResponse}
import com.lbs.server.actor.Book
import com.lbs.server.actor.StaticData.StaticDataConfig
import com.lbs.server.repository.model.{Bug, Monitoring}
import com.lbs.server.util.DateTimeUtil.{formatDate, formatDateTime, minutesSinceBeginOf2018}
object En extends Lang {
override def id: Int = 0
override def locale: Locale = Locale.ENGLISH
override def label: String = "🇺🇸󠁧󠁢󠁥󠁮󠁧󠁿English"
override protected def withPages(message: String, page: Int, pages: Int): String = {
if (pages > 1) s"$message. Page <b>${page + 1}</b> of <b>$pages</b>"
else message
}
override def unableToCancelUpcomingVisit(reason: String): String =
s"⚠ Unable to cancel upcoming visit! Reason: $reason"
override def appointmentHasBeenCancelled: String =
s"👍 Your appointment has been cancelled!"
override def yes: String = "Yes"
override def no: String = "No"
override def noUpcomingVisits: String =
" No upcoming visits found"
override def areYouSureToCancelAppointment(visit: ReservedVisit): String =
s"""<b>➡</b> Are you sure want to cancel appointment?
|
|⏱ <b>${formatDateTime(visit.visitDate.startDateTime, locale)}</b>
|${capitalizeFirstLetter(doctor)}: ${visit.doctorName}
|${capitalizeFirstLetter(service)}: ${visit.service.name}
|${capitalizeFirstLetter(clinic)}: ${visit.clinic.name}
|""".stripMargin
override def chooseDateFrom: String = "<b>➡</b> Please choose date from"
override def chooseDateTo: String = "<b>➡</b> Please choose date to"
override def findTerms: String = "🔍 Find terms"
override def modifyDate: String = "📅 Modify date"
override def bookingSummary(bookingData: Book.BookingData): String =
s"🦄 Ok! We are going to book a service <b>${bookingData.serviceId.name}</b>" +
s" with a doctor chosen <b>${bookingData.doctorId.name}</b>" +
s" in <b>${bookingData.clinicId.name}</b> clinic" +
s" of the <b>${bookingData.cityId.name}</b> city." +
s"\nDesired dates: <b>${formatDate(bookingData.dateFrom, locale)}</b> -> <b>${formatDate(bookingData.dateTo, locale)}</b>" +
s"\nTime: <b>${timeOfDay(bookingData.timeOfDay)}</b>" +
s"\n\n<b>➡</b> Now choose your action"
override def noTermsFound: String =
s""" No available terms found
|
|What do you want to do next?""".stripMargin
override def createMonitoring: String = "👀 Create monitoring"
override def cancel: String = "Cancel"
override def book: String = "Book"
override def confirmAppointment(term: AvailableVisitsTermPresentation, valuations: ValuationsResponse): String =
s"""<b>➡</b> ${valuations.optionsQuestion.getOrElse("Would you like to confirm your appointment booking?")}
|
|⏱ <b>${formatDateTime(term.visitDate.startDateTime, locale)}</b>
|${capitalizeFirstLetter(doctor)}: ${term.doctor.name}
|${capitalizeFirstLetter(clinic)}: ${term.clinic.name}
|
|${valuations.visitTermVariants.head.infoMessage}""".stripMargin
override def appointmentIsConfirmed: String = "👍 Your appointment has been confirmed!"
override def monitoringHasBeenCreated: String = "👍 Monitoring has been created! List of active /monitorings"
override def unableToCreateMonitoring: String = s"👎 Unable to create monitoring. Please create a /bug"
override def chooseTypeOfMonitoring: String = "<b>➡</b> Please choose type of monitoring you want"
override def bookByApplication: String = "👾 Book by application"
override def bookManually: String = "👤 Book manually"
override def city: String = "city"
override def clinic: String = "clinic"
override def service: String = "service"
override def doctor: String = "doctor"
override def previous: String = "Previous"
override def next: String = "Next"
override def noActiveMonitorings: String = " You don't have active monitorings. Create new one /book"
override def deactivateMonitoring(monitoring: Monitoring): String =
s"""<b>➡</b> Are you sure want to deactivate monitoring?
|
|📅 <b>${formatDate(monitoring.dateFrom, locale)}</b> -> <b>${formatDate(monitoring.dateTo, locale)}</b>
|⏱ <b>${timeOfDay(monitoring.timeOfDay)}</b>
|${capitalizeFirstLetter(doctor)}: ${monitoring.doctorName}
|${capitalizeFirstLetter(service)}: ${monitoring.serviceName}
|${capitalizeFirstLetter(clinic)}: ${monitoring.clinicName}""".stripMargin
override def deactivated: String = "👍 Deactivated! List of active /monitorings"
override def any: String = "Any"
override def pressAny: String = s"or press <b>$any</b> button"
override def pleaseEnterStaticDataNameOrAny(config: StaticDataConfig): String =
withAnyVariant(
s"""<b>➡</b> Please enter a ${config.name} name
|For example: <b>${config.example}</b>""".stripMargin,
config.isAnyAllowed)
override def pleaseEnterStaticDataNameOrPrevious(config: StaticDataConfig): String =
s"""<b>➡</b> Please enter a ${config.name} name
|For example: <b>${config.example}</b>
|
|or choose a ${config.name} from previous searches""".stripMargin
override def staticDataIs(config: StaticDataConfig, label: String): String =
s"<b>✅</b> ${capitalizeFirstLetter(config.name)} is <b>$label</b>"
override def pleaseChooseStaticDataNameOrAny(config: StaticDataConfig): String =
withAnyVariant(s"<b>➡</b> Please choose a ${config.name}", config.isAnyAllowed)
override def staticNotFound(config: StaticDataConfig): String =
withAnyVariant(
s"""<b>➡</b> Nothing was found 😔
|Please enter a ${config.name} name again""", config.isAnyAllowed)
override def loginAndPasswordAreOk: String =
s"""✅ Congrats! Login and password are OK!
|Now you can change the language /settings
""".stripMargin
override def provideUsername: String =
s""" You must be logged in using your <b>Luxmed</b> credentials
|
|<b>➡</b> Please provide username""".stripMargin
override def providePassword: String = "<b>➡</b> Please provide password"
override def visitsHistoryIsEmpty: String = " No visits in your history"
override def help: String =
s""" This is non official bot for Portal Pacienta LUX MED.
|With its help you can book a visit to the doctor, create term monitorings, view upcoming visits and visit history
|
|<b>➡</b> Supported commands
|/login - enter Luxmed credentials
|/book - make an appointment
|/monitorings - available terms monitoring
|/history - visits history
|/visits - upcoming visits
|/settings - change language
|/bug - submit an issue""".stripMargin
override def dateFromIs(dateFrom: ZonedDateTime): String = s"📅 Date from is ${formatDate(dateFrom, locale)}"
override def dateToIs(dateTo: ZonedDateTime): String = s"📅 Date to is ${formatDate(dateTo, locale)}"
override def termEntry(term: AvailableVisitsTermPresentation, page: Int, index: Int): String =
s"""⏱ <b>${formatDateTime(term.visitDate.startDateTime, locale)}</b>
|${capitalizeFirstLetter(doctor)}: ${term.doctor.name}
|${capitalizeFirstLetter(clinic)}: ${term.clinic.name}
|<b>➡</b> /book_${page}_$index
|
|""".stripMargin
override def termsHeader(page: Int, pages: Int): String =
withPages("<b>➡</b> Available terms", page, pages)
override def historyEntry(visit: HistoricVisit, page: Int, index: Int): String =
s"""⏱ <b>${formatDateTime(visit.visitDate.startDateTime, locale)}</b>
|${capitalizeFirstLetter(doctor)}: ${visit.doctorName}
|${capitalizeFirstLetter(service)}: ${visit.service.name}
|${capitalizeFirstLetter(clinic)}: ${visit.clinicName}
|<b>➡</b> /repeat_${page}_$index
|
|""".stripMargin
override def historyHeader(page: Int, pages: Int): String =
withPages("<b>➡</b> Conducted visits", page, pages)
override def upcomingVisitEntry(visit: ReservedVisit, page: Int, index: Int): String =
s"""⏱ <b>${formatDateTime(visit.visitDate.startDateTime, locale)}</b>
|${capitalizeFirstLetter(doctor)}: ${visit.doctorName}
|${capitalizeFirstLetter(service)}: ${visit.service.name}
|${capitalizeFirstLetter(clinic)}: ${visit.clinic.name}
|<b>➡</b> /cancel_${page}_$index
|
|""".stripMargin
override def upcomingVisitsHeader(page: Int, pages: Int): String =
withPages("<b>➡</b> Reserved visits", page, pages)
override def bugEntry(bug: Bug, page: Int, index: Int): String =
s"""⏱ <b>${formatDateTime(bug.submitted, locale)}</b>
|Description: ${bug.details}
|State: <b>${if (bug.resolved) "✅ Resolved" else "🚫 Unresolved"}</b>
|
|""".stripMargin
override def bugsHeader(page: Int, pages: Int): String =
withPages("<b>➡</b> Submitted issues", page, pages)
override def monitoringEntry(monitoring: Monitoring, page: Int, index: Int): String =
s"""📅 <b>${formatDate(monitoring.dateFrom, locale)}</b> -> <b>${formatDate(monitoring.dateTo, locale)}</b>
|⏱ <b>${timeOfDay(monitoring.timeOfDay)}</b>
|${capitalizeFirstLetter(doctor)}: ${monitoring.doctorName}
|${capitalizeFirstLetter(service)}: ${monitoring.serviceName}
|${capitalizeFirstLetter(clinic)}: ${monitoring.clinicName}
|${capitalizeFirstLetter(city)}: ${monitoring.cityName}
|Type: ${if (monitoring.autobook) "Auto" else "Manual"}
|<b>➡</b> /cancel_${page}_$index
|
|""".stripMargin
override def monitoringsHeader(page: Int, pages: Int): String =
s"<b>➡</b> Active monitorings."
override def invalidLoginOrPassword: String =
"""❗ You have entered invalid login or password or changed it via site.
|Your monitorings were removed. Please /login again and create new monitorings.
""".stripMargin
override def availableTermEntry(term: AvailableVisitsTermPresentation, monitoring: Monitoring, index: Int): String =
s"""⏱ <b>${formatDateTime(term.visitDate.startDateTime, locale)}</b>
|${capitalizeFirstLetter(doctor)}: ${term.doctor.name}
|${capitalizeFirstLetter(service)}: ${monitoring.serviceName}
|${capitalizeFirstLetter(clinic)}: ${term.clinic.name}
|${capitalizeFirstLetter(city)}: ${monitoring.cityName}
|/reserve_${monitoring.recordId}_${term.scheduleId}_${minutesSinceBeginOf2018(term.visitDate.startDateTime)}
|
|""".stripMargin
override def availableTermsHeader(size: Int): String =
s"""✅ <b>$size</b> terms were found by monitoring. We showed you the closest 5.
|<b>➡</b> Please choose one to reserve""".stripMargin
override def nothingWasFoundByMonitoring(monitoring: Monitoring): String =
s"""❗ Nothing was found by your monitoring. Monitoring has been <b>disabled</b> as outdated.
|
|📅 <b>${formatDate(monitoring.dateFrom, locale)}</b> -> <b>${formatDate(monitoring.dateTo, locale)}</b>
|⏱ <b>${timeOfDay(monitoring.timeOfDay)}</b>
|${capitalizeFirstLetter(doctor)}: ${monitoring.doctorName}
|${capitalizeFirstLetter(service)}: ${monitoring.serviceName}
|${capitalizeFirstLetter(clinic)}: ${monitoring.clinicName}
|${capitalizeFirstLetter(city)}: ${monitoring.cityName}
|
|<b>➡</b> Create new monitoring /book""".stripMargin
override def appointmentIsBooked(term: AvailableVisitsTermPresentation, monitoring: Monitoring): String =
s"""👍 We just booked appointment for you!
|
|⏱ <b>${formatDateTime(term.visitDate.startDateTime, locale)}</b>
|${capitalizeFirstLetter(doctor)}: ${term.doctor.name}
|${capitalizeFirstLetter(service)}: ${monitoring.serviceName}
|${capitalizeFirstLetter(clinic)}: ${term.clinic.name}
|${capitalizeFirstLetter(city)}: ${monitoring.cityName}""".stripMargin
override def maximumMonitoringsLimitExceeded: String = "Maximum monitorings per user is 5"
override def monitoringOfTheSameTypeExists: String = "You already have active monitoring for the same service /monitorings"
override def termIsOutdated: String =
s"""❗️ Looks like the term is already booked by someone else
|Please try another one or create a new monitoring /book""".stripMargin
override def loginHasChangedOrWrong: String =
"""❗ You have entered invalid <b>login</b> or <b>password</b> or changed it via site.
|Please /login again and create a new monitoring /book.
""".stripMargin
override def settingsHeader: String = "<b>➡</b> Please choose an action"
override def language: String = "Change language"
override def chooseLanguage: String = "<b>➡</b> Please choose a language"
override def languageUpdated: String = "👍 Language was successfully changed!"
override def appointmentWasNotCancelled: String = "👍 Appointment was not cancelled"
override def monitoringWasNotDeactivated: String = "👍 Monitoring was not deactivated"
override def bugAction: String = "<b>➡</b> Please choose an action"
override def createNewBug: String = "🐞 Submit new"
override def showSubmittedBugs: String = "👀 Show submitted"
override def enterIssueDetails: String = "<b>➡</b> Please provide issue details"
override def noSubmittedIssuesFound: String = " No submitted issues found"
override def bugHasBeenCreated(bugId: Long): String = s"✅ Thank you for submitting bug <b>#$bugId</b>!"
override def chooseTimeOfDay: String = "<b>➡</b> Please choose preferred time of day"
override def afterFive: String = "After 17:00"
override def nineToFive: String = "From 09:00 to 17:00"
override def beforeNine: String = "Before 09:00"
override def allDay: String = "All day"
override def preferredTimeIs(time: Int): String = s"⏱ Preferred time is ${timeOfDay(time)}"
}

View File

@@ -0,0 +1,236 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.lang
import java.time.ZonedDateTime
import java.util.Locale
import com.lbs.api.json.model.{AvailableVisitsTermPresentation, HistoricVisit, ReservedVisit, ValuationsResponse}
import com.lbs.server.actor.Book.BookingData
import com.lbs.server.actor.StaticData.StaticDataConfig
import com.lbs.server.repository.model
import com.lbs.server.repository.model.Monitoring
object Lang {
val Langs: Seq[Lang] = Seq(En, Ua)
private val LangsMap = Seq(En, Ua).map(e => e.id -> e).toMap
def apply(id: Int): Lang = {
LangsMap.getOrElse(id, sys.error(s"Unknown language id $id"))
}
}
trait Lang {
def id: Int
def locale: Locale
def label: String
val timeOfDay = Map(
0 -> allDay,
1 -> beforeNine,
2 -> nineToFive,
3 -> afterFive
)
protected def capitalizeFirstLetter(str: String): String = {
val fistCapitalLetter = str.head.toTitleCase
fistCapitalLetter + str.tail
}
protected def withPages(message: String, page: Int, pages: Int): String
def unableToCancelUpcomingVisit(reason: String): String
def appointmentHasBeenCancelled: String
def yes: String
def no: String
def noUpcomingVisits: String
def areYouSureToCancelAppointment(visit: ReservedVisit): String
def chooseDateFrom: String
def chooseDateTo: String
def findTerms: String
def modifyDate: String
def bookingSummary(bookingData: BookingData): String
def noTermsFound: String
def createMonitoring: String
def cancel: String
def book: String
def confirmAppointment(term: AvailableVisitsTermPresentation, valuations: ValuationsResponse): String
def appointmentIsConfirmed: String
def monitoringHasBeenCreated: String
def unableToCreateMonitoring: String
def chooseTypeOfMonitoring: String
def bookByApplication: String
def bookManually: String
def city: String
def clinic: String
def service: String
def doctor: String
def previous: String
def next: String
def noActiveMonitorings: String
def deactivateMonitoring(monitoring: Monitoring): String
def deactivated: String
def any: String
def pressAny: String
protected def withAnyVariant(message: String, isAnyAllowed: Boolean): String = {
if (isAnyAllowed)
message + "\n\n" + pressAny
else message
}
def pleaseEnterStaticDataNameOrAny(config: StaticDataConfig): String
def pleaseEnterStaticDataNameOrPrevious(config: StaticDataConfig): String
def staticDataIs(config: StaticDataConfig, label: String): String
def pleaseChooseStaticDataNameOrAny(config: StaticDataConfig): String
def staticNotFound(config: StaticDataConfig): String
def loginAndPasswordAreOk: String
def provideUsername: String
def providePassword: String
def visitsHistoryIsEmpty: String
def help: String
def dateFromIs(dateFrom: ZonedDateTime): String
def dateToIs(dateTo: ZonedDateTime): String
def termEntry(term: AvailableVisitsTermPresentation, page: Int, index: Int): String
def termsHeader(page: Int, pages: Int): String
def historyEntry(visit: HistoricVisit, page: Int, index: Int): String
def historyHeader(page: Int, pages: Int): String
def upcomingVisitEntry(visit: ReservedVisit, page: Int, index: Int): String
def upcomingVisitsHeader(page: Int, pages: Int): String
def bugEntry(bug: model.Bug, page: Int, index: Int): String
def bugsHeader(page: Int, pages: Int): String
def monitoringEntry(monitoring: Monitoring, page: Int, index: Int): String
def monitoringsHeader(page: Int, pages: Int): String
def invalidLoginOrPassword: String
def availableTermEntry(term: AvailableVisitsTermPresentation, monitoring: Monitoring, index: Int): String
def availableTermsHeader(size: Int): String
def nothingWasFoundByMonitoring(monitoring: Monitoring): String
def appointmentIsBooked(term: AvailableVisitsTermPresentation, monitoring: Monitoring): String
def maximumMonitoringsLimitExceeded: String
def monitoringOfTheSameTypeExists: String
def termIsOutdated: String
def loginHasChangedOrWrong: String
def settingsHeader: String
def language: String
def chooseLanguage: String
def languageUpdated: String
def appointmentWasNotCancelled: String
def monitoringWasNotDeactivated: String
def createNewBug: String
def showSubmittedBugs: String
def bugAction: String
def bugHasBeenCreated(bugId: Long): String
def noSubmittedIssuesFound: String
def enterIssueDetails: String
def chooseTimeOfDay: String
def afterFive: String
def nineToFive: String
def beforeNine: String
def allDay: String
def preferredTimeIs(time: Int): String
}

View File

@@ -0,0 +1,34 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.lang
import com.lbs.server.actor.Login.UserId
trait Localizable {
protected def userId: UserId
protected def localization: Localization
protected def lang: Lang = Option(userId).map(uId => localization.lang(uId.userId)).getOrElse(En)
}

View File

@@ -0,0 +1,63 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.lang
import java.util.concurrent.ConcurrentHashMap
import com.lbs.server.repository.model
import com.lbs.server.service.DataService
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
@Component
class Localization {
@Autowired
private var dataService: DataService = _
private val cachedLangs = new ConcurrentHashMap[Long, Lang]
def lang(userId: Long): Lang = {
cachedLangs.computeIfAbsent(userId, _ => {
val settings = dataService.findSettings(userId)
settings.map(s => Lang(s.lang)).getOrElse(En)
})
}
def invalidateLangsCache(): Unit = {
cachedLangs.clear()
}
def updateLanguage(userId: Long, lang: Lang): Unit = {
cachedLangs.put(userId, lang)
val settings = dataService.findSettings(userId) match {
case Some(exists) =>
exists.setLang(lang.id)
exists
case None => model.Settings(userId, lang.id)
}
dataService.saveSettings(settings)
}
}

View File

@@ -0,0 +1,350 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.lang
import java.time.ZonedDateTime
import java.util.Locale
import com.lbs.api.json.model.{AvailableVisitsTermPresentation, HistoricVisit, ReservedVisit, ValuationsResponse}
import com.lbs.server.actor.Book
import com.lbs.server.actor.StaticData.StaticDataConfig
import com.lbs.server.repository.model.{Bug, Monitoring}
import com.lbs.server.util.DateTimeUtil.{formatDate, formatDateTime, minutesSinceBeginOf2018}
object Ua extends Lang {
override def id: Int = 1
override def locale: Locale = new Locale("uk", "UA")
override def label: String = "🇺🇦Українська"
override protected def withPages(message: String, page: Int, pages: Int): String = {
if (pages > 1) s"$message. Сторінка <b>${page + 1}</b> з <b>$pages</b>"
else message
}
override def unableToCancelUpcomingVisit(reason: String): String =
s"Не вдається скасувати візит! Причина: $reason"
override def appointmentHasBeenCancelled: String =
s"👍 Ваш візит було скасовано!"
override def yes: String = "Так"
override def no: String = "Ні"
override def noUpcomingVisits: String =
" Не знайдено жодного майбутнього візиту"
override def areYouSureToCancelAppointment(visit: ReservedVisit): String =
s"""<b>➡</b> Ви впевнені, що хочете скасувати візит?
|
|⏱ <b>${formatDateTime(visit.visitDate.startDateTime, locale)}</b>
|${capitalizeFirstLetter(doctor)}: ${visit.doctorName}
|${capitalizeFirstLetter(service)}: ${visit.service.name}
|${capitalizeFirstLetter(clinic)}: ${visit.clinic.name}
|""".stripMargin
override def chooseDateFrom: String = "<b>➡</b> Будь ласка, виберіть початкову дату"
override def chooseDateTo: String = "<b>➡</b> Будь ласка, виберіть кінцеву дату"
override def findTerms: String = "🔍 Знайти терміни"
override def modifyDate: String = "📅 Змінити дату"
override def bookingSummary(bookingData: Book.BookingData): String =
s"🦄 Супер! Ми збираємося зарезервувати послугу <b>${bookingData.serviceId.name}</b>" +
s" з обраним лікарем <b>${bookingData.doctorId.name}</b>" +
s" в <b>${bookingData.clinicId.name}</b> клініці" +
s" міста <b>${bookingData.cityId.name}</b>." +
s"\nБажані дати: <b>${formatDate(bookingData.dateFrom, locale)}</b> -> <b>${formatDate(bookingData.dateTo, locale)}</b>" +
s"\nЧас: <b>${timeOfDay(bookingData.timeOfDay)}</b>" +
s"\n\n<b>➡</b> Тепер оберіть наступну дію"
override def noTermsFound: String =
s""" Терміни відсутні
|
|Що ви хочете зробити далі?""".stripMargin
override def createMonitoring: String = "👀 Створити моніторінг"
override def cancel: String = "Відмінити"
override def book: String = "Зарезервувати"
override def confirmAppointment(term: AvailableVisitsTermPresentation, valuations: ValuationsResponse): String =
s"""<b>➡</b> ${valuations.optionsQuestion.getOrElse("Ви хотіли б підтвердити резервацію візиту?")}
|
|⏱ <b>${formatDateTime(term.visitDate.startDateTime, locale)}</b>
|${capitalizeFirstLetter(doctor)}: ${term.doctor.name}
|${capitalizeFirstLetter(clinic)}: ${term.clinic.name}
|
|${valuations.visitTermVariants.head.infoMessage}""".stripMargin
override def appointmentIsConfirmed: String = "👍 Ваш візит було підтверджено!"
override def monitoringHasBeenCreated: String = "👍 Моніторинг був створений! Список активних /monitorings"
override def unableToCreateMonitoring: String = s"👎 Не вдається створити моніторинг. Будь ласка, створіть /bug"
override def chooseTypeOfMonitoring: String = "<b>➡</b> Будь ласка, виберіть тип моніторингу"
override def bookByApplication: String = "👾 Автоматична резервація"
override def bookManually: String = "👤 Ручна резервація"
override def city: String = "місто"
override def clinic: String = "клініка"
override def service: String = "послуга"
override def doctor: String = "лікар"
override def previous: String = "Попередня"
override def next: String = "Наступна"
override def noActiveMonitorings: String = " У вас немає активних моніторингів. Створити новий /book"
override def deactivateMonitoring(monitoring: Monitoring): String =
s"""<b>➡</b> Ви впевнені, що хочете вимкнути моніторинг?
|
|📅 <b>${formatDate(monitoring.dateFrom, locale)}</b> -> <b>${formatDate(monitoring.dateTo, locale)}</b>
|⏱ <b>${timeOfDay(monitoring.timeOfDay)}</b>
|${capitalizeFirstLetter(doctor)}: ${monitoring.doctorName}
|${capitalizeFirstLetter(service)}: ${monitoring.serviceName}
|${capitalizeFirstLetter(clinic)}: ${monitoring.clinicName}""".stripMargin
override def deactivated: String = "👍 Деактивовано! Список активних /monitorings"
override def any: String = "Будь-який"
override def pressAny: String = s"або натисніть кнопку <b>$any</b>"
override def pleaseEnterStaticDataNameOrAny(config: StaticDataConfig): String =
withAnyVariant(
s"""<b>➡</b> Будь ласка, введіть ${config.name}
|Наприклад: <b>${config.example}</b>""".stripMargin,
config.isAnyAllowed)
override def pleaseEnterStaticDataNameOrPrevious(config: StaticDataConfig): String =
s"""<b>➡</b> Будь ласка, введіть ${config.name}
|Наприклад: <b>${config.example}</b>
|
|або виберіть ${config.name} з попередніх пошуків""".stripMargin
override def staticDataIs(config: StaticDataConfig, label: String): String =
s"<b>✅</b> ${capitalizeFirstLetter(config.name)} <b>$label</b>"
override def pleaseChooseStaticDataNameOrAny(config: StaticDataConfig): String =
withAnyVariant(s"<b>➡</b> Будь ласка, виберіть ${config.name}", config.isAnyAllowed)
override def staticNotFound(config: StaticDataConfig): String =
withAnyVariant(
s"""<b>➡</b> Нічого не знайдено 😔
|Будь ласка, введіть ${config.name} знову""".stripMargin, config.isAnyAllowed)
override def loginAndPasswordAreOk: String =
s"""✅ Супер! Логін і пароль збережено
|Тепер ви можете змінити мову /settings""".stripMargin
override def provideUsername: String =
s""" Ви повинні увійти в систему, використовуючи облікові дані <b>Luxmed</b>
|
|<b>➡</b> Будь ласка, введіть ім'я користувача""".stripMargin
override def providePassword: String = "<b>➡</b> Будь ласка, введіть пароль"
override def visitsHistoryIsEmpty: String = " Немає візитів в вашій історії"
override def help: String =
s""" Це неофіційний бот для Порталу Пацієнта LUX MED.
|Завдяки йому ви можете зарезервувати візит до лікаря, створити моніторинг доступних термінів, переглянути історію та майбутні візити
|
|<b>➡</b> Підтримувані команди
|/login - ввести облікові дані Luxmed
|/book - призначити візит
|/monitorings - моніторінг доступних термінів
|/history - історія візитів
|/visits - майбутні візити
|/settings - змінити мову
|/bug - відправити баг""".stripMargin
override def dateFromIs(dateFrom: ZonedDateTime): String = s"📅 Початкова дата ${formatDate(dateFrom, locale)}"
override def dateToIs(dateTo: ZonedDateTime): String = s"📅 Кінцева дата ${formatDate(dateTo, locale)}"
override def termEntry(term: AvailableVisitsTermPresentation, page: Int, index: Int): String =
s"""⏱ <b>${formatDateTime(term.visitDate.startDateTime, locale)}</b>
|${capitalizeFirstLetter(doctor)}: ${term.doctor.name}
|${capitalizeFirstLetter(clinic)}: ${term.clinic.name}
|<b>➡</b> /book_${page}_$index
|
|""".stripMargin
override def termsHeader(page: Int, pages: Int): String =
withPages("<b>➡</b> Доступні терміни", page, pages)
override def historyEntry(visit: HistoricVisit, page: Int, index: Int): String =
s"""⏱ <b>${formatDateTime(visit.visitDate.startDateTime, locale)}</b>
|${capitalizeFirstLetter(doctor)}: ${visit.doctorName}
|${capitalizeFirstLetter(service)}: ${visit.service.name}
|${capitalizeFirstLetter(clinic)}: ${visit.clinicName}
|<b>➡</b> /repeat_${page}_$index
|
|""".stripMargin
override def historyHeader(page: Int, pages: Int): String =
withPages("<b>➡</b> Завершені візити", page, pages)
override def upcomingVisitEntry(visit: ReservedVisit, page: Int, index: Int): String =
s"""⏱ <b>${formatDateTime(visit.visitDate.startDateTime, locale)}</b>
|${capitalizeFirstLetter(doctor)}: ${visit.doctorName}
|${capitalizeFirstLetter(service)}: ${visit.service.name}
|${capitalizeFirstLetter(clinic)}: ${visit.clinic.name}
|<b>➡</b> /cancel_${page}_$index
|
|""".stripMargin
override def upcomingVisitsHeader(page: Int, pages: Int): String =
withPages("<b>➡</b> Зарезервовані візити", page, pages)
override def bugEntry(bug: Bug, page: Int, index: Int): String =
s"""⏱ <b>${formatDateTime(bug.submitted, locale)}</b>
|Опис: ${bug.details}
|Статус: <b>${if (bug.resolved) "✅ Вирішено" else "🚫 Невирішено"}</b>
|
|""".stripMargin
override def bugsHeader(page: Int, pages: Int): String =
withPages("<b>➡</b> Створені баги", page, pages)
override def monitoringEntry(monitoring: Monitoring, page: Int, index: Int): String =
s"""📅 <b>${formatDate(monitoring.dateFrom, locale)}</b> -> <b>${formatDate(monitoring.dateTo, locale)}</b>
|⏱ <b>${timeOfDay(monitoring.timeOfDay)}</b>
|${capitalizeFirstLetter(doctor)}: ${monitoring.doctorName}
|${capitalizeFirstLetter(service)}: ${monitoring.serviceName}
|${capitalizeFirstLetter(clinic)}: ${monitoring.clinicName}
|${capitalizeFirstLetter(city)}: ${monitoring.cityName}
|Тип: ${if (monitoring.autobook) "Автоматичний" else "Ручний"}
|<b>➡</b> /cancel_${page}_$index
|
|""".stripMargin
override def monitoringsHeader(page: Int, pages: Int): String =
s"<b>➡</b> Активні моніторінги"
override def invalidLoginOrPassword: String =
"""❗ Ви ввели невірний логін або пароль, або змінили його через сайт.
|Ваші моніторинги були видалені. Будь ласка, /login знову і створіть нові моніторинги.
""".stripMargin
override def availableTermEntry(term: AvailableVisitsTermPresentation, monitoring: Monitoring, index: Int): String =
s"""⏱ <b>${formatDateTime(term.visitDate.startDateTime, locale)}</b>
|${capitalizeFirstLetter(doctor)}: ${term.doctor.name}
|${capitalizeFirstLetter(service)}: ${monitoring.serviceName}
|${capitalizeFirstLetter(clinic)}: ${term.clinic.name}
|${capitalizeFirstLetter(city)}: ${monitoring.cityName}
|/reserve_${monitoring.recordId}_${term.scheduleId}_${minutesSinceBeginOf2018(term.visitDate.startDateTime)}
|
|""".stripMargin
override def availableTermsHeader(size: Int): String =
s"""✅ <b>$size</b> термінів було знайдено за допомогою моніторінгу. Ми показали вам найближчі 5.
|<b>➡</b> Будь ласка, оберіть один щоб заререзвувати""".stripMargin
override def nothingWasFoundByMonitoring(monitoring: Monitoring): String =
s"""❗ Нічого не знайдено за вашим моніторингом. Моніторинг був <b>вимкнений</b> як застарілий.
|
|📅 <b>${formatDate(monitoring.dateFrom, locale)}</b> -> <b>${formatDate(monitoring.dateTo, locale)}</b>
|⏱ <b>${timeOfDay(monitoring.timeOfDay)}</b>
|${capitalizeFirstLetter(doctor)}: ${monitoring.doctorName}
|${capitalizeFirstLetter(service)}: ${monitoring.serviceName}
|${capitalizeFirstLetter(clinic)}: ${monitoring.clinicName}
|${capitalizeFirstLetter(city)}: ${monitoring.cityName}
|
|<b>➡</b> Створити новий моніторінг /book""".stripMargin
override def appointmentIsBooked(term: AvailableVisitsTermPresentation, monitoring: Monitoring): String =
s"""👍 Ми зерезевували візит для вас!
|
|⏱ <b>${formatDateTime(term.visitDate.startDateTime, locale)}</b>
|${capitalizeFirstLetter(doctor)}: ${term.doctor.name}
|${capitalizeFirstLetter(service)}: ${monitoring.serviceName}
|${capitalizeFirstLetter(clinic)}: ${term.clinic.name}
|${capitalizeFirstLetter(city)}: ${monitoring.cityName}""".stripMargin
override def maximumMonitoringsLimitExceeded: String = "Максимальна кількість моніторінгів 5"
override def monitoringOfTheSameTypeExists: String = "У вас вже є активний моніторинг на таку ж саму послугу /monitorings"
override def termIsOutdated: String =
s"""❗️ Схоже, що термін вже не є доступним
|Будь ласка, спробуйте інший або створіть новий моніторинг /book""".stripMargin
override def loginHasChangedOrWrong: String =
"""❗ Ви ввели невірний і <b>логін</b> або <b>пароль</b> або змінили його через сайт.
|Будь ласка, /login знову і створіть новий моніторинг/book.
""".stripMargin
override def settingsHeader: String = "<b>➡</b> Оберіть дію"
override def language: String = "Змінтини мову"
override def chooseLanguage: String = "<b>➡</b> Будь ласка, оберіть мову"
override def languageUpdated: String = "👍 Мову успішно змінено!"
override def appointmentWasNotCancelled: String = "👍 Візит не було скасовано"
override def monitoringWasNotDeactivated: String = "👍 Моніторінг не було деактивовано"
override def bugAction: String = "<b>➡</b> Оберіть дію"
override def createNewBug: String = "🐞 Створити новий"
override def showSubmittedBugs: String = "👀 Показати створені"
override def enterIssueDetails: String = "<b>➡</b> Будь ласка, введіть деталі проблеми"
override def noSubmittedIssuesFound: String = " Створених вами багів не знайдено"
override def bugHasBeenCreated(bugId: Long): String = s"✅ Дякуємо за відправлений баг <b>#$bugId</b>!"
override def chooseTimeOfDay: String = "<b>➡</b> Будь ласка, оберіть бажаний час"
override def afterFive: String = "Після 17:00"
override def nineToFive: String = "Від 09:00 до 17:00"
override def beforeNine: String = "До 09:00"
override def allDay: String = "Весь день"
override def preferredTimeIs(time: Int): String = s"⏱ Бажаний час ${timeOfDay(time)}"
}

View File

@@ -0,0 +1,192 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.repository
import java.time.ZonedDateTime
import com.lbs.server.repository.model.{Bug, CityHistory, ClinicHistory, Credentials, DoctorHistory, JLong, Monitoring, ServiceHistory, Settings, Source}
import javax.persistence.EntityManager
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Repository
import scala.collection.JavaConverters._
@Repository
class DataRepository(@Autowired em: EntityManager) {
private val maxHistory = 2
def getCityHistory(userId: Long): Seq[CityHistory] = {
em.createQuery(
"""select city from CityHistory city where city.recordId in
| (select max(c.recordId) from CityHistory c where c.userId = :userId group by c.name order by MAX(c.time) desc)
| order by city.time desc""".stripMargin, classOf[CityHistory])
.setParameter("userId", userId)
.setMaxResults(maxHistory)
.getResultList.asScala
}
def getClinicHistory(userId: Long, cityId: Long): Seq[ClinicHistory] = {
em.createQuery(
"""select clinic from ClinicHistory clinic where clinic.recordId in
| (select max(c.recordId) from ClinicHistory c where c.userId = :userId and c.cityId = :cityId group by c.name order by MAX(c.time) desc)
| order by clinic.time desc""".stripMargin, classOf[ClinicHistory])
.setParameter("userId", userId)
.setParameter("cityId", cityId)
.setMaxResults(maxHistory)
.getResultList.asScala
}
def getServiceHistory(userId: Long, cityId: Long, clinicId: Option[Long]): Seq[ServiceHistory] = {
val query = em.createQuery(
s"""select service from ServiceHistory service where service.recordId in
| (select max(s.recordId) from ServiceHistory s where s.userId = :userId and s.cityId = :cityId
| and s.clinicId ${clinicId.map(_ => "= :clinicId").getOrElse("IS NULL")} group by s.name order by MAX(s.time) desc)
| order by service.time desc""".stripMargin, classOf[ServiceHistory])
.setParameter("userId", userId)
.setParameter("cityId", cityId)
.setMaxResults(maxHistory)
clinicId.map(id => query.setParameter("clinicId", id)).getOrElse(query).getResultList.asScala
}
def getDoctorHistory(userId: Long, cityId: Long, clinicId: Option[Long], serviceId: Long): Seq[DoctorHistory] = {
val query = em.createQuery(
s"""select doctor from DoctorHistory doctor where doctor.recordId in
| (select max(d.recordId) from DoctorHistory d where d.userId = :userId
| and d.cityId = :cityId and d.clinicId ${clinicId.map(_ => "= :clinicId").getOrElse("IS NULL")}
| and d.serviceId = :serviceId group by d.name order by MAX(d.time) desc)
| order by doctor.time desc""".stripMargin, classOf[DoctorHistory])
.setParameter("userId", userId)
.setParameter("cityId", cityId)
.setParameter("serviceId", serviceId)
.setMaxResults(maxHistory)
clinicId.map(id => query.setParameter("clinicId", id)).getOrElse(query).getResultList.asScala
}
def findCredentials(userId: Long): Option[Credentials] = {
em.createQuery(
"select credentials from Credentials credentials where credentials.userId = :userId", classOf[Credentials])
.setParameter("userId", userId)
.getResultList.asScala.headOption
}
def getBugs(userId: Long): Seq[Bug] = {
em.createQuery(
"""select bug from Bug bug where bug.userId = :userId order by bug.submitted desc""".stripMargin, classOf[Bug])
.setParameter("userId", userId)
.setMaxResults(50)
.getResultList.asScala
}
def getActiveMonitorings: Seq[Monitoring] = {
em.createQuery(
"""select monitoring from Monitoring monitoring where monitoring.active = true""".stripMargin, classOf[Monitoring])
.getResultList.asScala
}
def getActiveMonitoringsCount(userId: Long): JLong = {
em.createQuery(
"""select count(monitoring) from Monitoring monitoring where monitoring.active = true
| and monitoring.userId = :userId""".stripMargin, classOf[JLong])
.setParameter("userId", userId)
.getSingleResult
}
def getActiveMonitorings(userId: Long): Seq[Monitoring] = {
em.createQuery(
"""select monitoring from Monitoring monitoring where monitoring.active = true
| and monitoring.userId = :userId order by monitoring.dateTo asc""".stripMargin, classOf[Monitoring])
.setParameter("userId", userId)
.getResultList.asScala
}
def findActiveMonitoring(userId: Long, cityId: Long, serviceId: Long): Option[Monitoring] = {
em.createQuery(
"""select monitoring from Monitoring monitoring where monitoring.active = true
| and monitoring.userId = :userId
| and monitoring.cityId = :cityId
| and monitoring.serviceId = :serviceId""".stripMargin, classOf[Monitoring])
.setParameter("userId", userId)
.setParameter("cityId", cityId)
.setParameter("serviceId", serviceId)
.getResultList.asScala.headOption
}
def getActiveMonitoringsSince(since: ZonedDateTime): Seq[Monitoring] = {
em.createQuery(
"""select monitoring from Monitoring monitoring where monitoring.active = true
| and monitoring.created > :since""".stripMargin, classOf[Monitoring])
.setParameter("since", since)
.getResultList.asScala
}
def findMonitoring(userId: Long, monitoringId: Long): Option[Monitoring] = {
em.createQuery(
"""select monitoring from Monitoring monitoring where monitoring.userId = :userId
| and monitoring.recordId = :monitoringId""".stripMargin, classOf[Monitoring])
.setParameter("userId", userId)
.setParameter("monitoringId", monitoringId)
.getResultList.asScala.headOption
}
def findSettings(userId: Long): Option[Settings] = {
em.createQuery(
"select settings from Settings settings where settings.userId = :userId", classOf[Settings])
.setParameter("userId", userId)
.getResultList.asScala.headOption
}
def findUserId(chatId: String, sourceSystemId: Long): Option[JLong] = {
em.createQuery(
"select source.userId from Source source where source.chatId = :chatId" +
" and source.sourceSystemId = :sourceSystemId", classOf[JLong])
.setParameter("chatId", chatId)
.setParameter("sourceSystemId", sourceSystemId)
.getResultList.asScala.headOption
}
def findCredentialsByUsername(username: String): Option[Credentials] = {
em.createQuery(
"select credentials from Credentials credentials where credentials.username = :username", classOf[Credentials])
.setParameter("username", username)
.getResultList.asScala.headOption
}
def findSource(chatId: String, sourceSystemId: Long, userId: Long): Option[Source] = {
em.createQuery(
"select source from Source source where source.chatId = :chatId" +
" and source.sourceSystemId = :sourceSystemId" +
" and userId = :userId", classOf[Source])
.setParameter("chatId", chatId)
.setParameter("sourceSystemId", sourceSystemId)
.setParameter("userId", userId)
.getResultList.asScala.headOption
}
def saveEntity[T](entity: T): T = {
em.merge(entity)
}
}

View File

@@ -0,0 +1,68 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.repository.model
import java.time.ZonedDateTime
import javax.persistence.{Access, AccessType, Column, Entity}
import scala.beans.BeanProperty
@Entity
@Access(AccessType.FIELD)
class Bug extends RecordId {
@BeanProperty
@Column(name = "user_id", nullable = false)
var userId: JLong = _
@BeanProperty
@Column(name = "source_system_id", nullable = false)
var sourceSystemId: JLong = _
@BeanProperty
@Column(nullable = false)
var details: String = _
@BeanProperty
@Column(nullable = false)
var resolved: Boolean = false
@BeanProperty
@Column(nullable = false)
var submitted: ZonedDateTime = ZonedDateTime.now()
}
object Bug {
def apply(userId: Long, sourceSystemId: Long, details: String, resolved: Boolean = false, submitted: ZonedDateTime = ZonedDateTime.now()): Bug = {
val bug = new Bug
bug.userId = userId
bug.sourceSystemId = sourceSystemId
bug.details = details
bug.resolved = resolved
bug.submitted = submitted
bug
}
}

View File

@@ -0,0 +1,61 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.repository.model
import java.time.ZonedDateTime
import javax.persistence.{Access, AccessType, Column, Entity}
import scala.beans.BeanProperty
@Entity
@Access(AccessType.FIELD)
class CityHistory extends History with RecordId {
@BeanProperty
@Column(nullable = false)
var id: JLong = _
@BeanProperty
@Column(nullable = false)
var name: String = _
@BeanProperty
@Column(name = "user_id", nullable = false)
var userId: JLong = _
@BeanProperty
@Column(nullable = false)
var time: ZonedDateTime = _
}
object CityHistory {
def apply(userId: Long, id: Long, name: String, time: ZonedDateTime): CityHistory = {
val city = new CityHistory
city.userId = userId
city.id = id
city.name = name
city.time = time
city
}
}

View File

@@ -0,0 +1,66 @@
/**
* MIT License
*
* Copyright (c) 2018 Yevhen Zadyra
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.lbs.server.repository.model
import java.time.ZonedDateTime
import javax.persistence.{Access, AccessType, Column, Entity}
import scala.beans.BeanProperty
@Entity
@Access(AccessType.FIELD)
class ClinicHistory extends History with RecordId {
@BeanProperty
@Column(nullable = false)
var id: JLong = _
@BeanProperty
@Column(nullable = false)
var name: String = _
@BeanProperty
@Column(name = "user_id", nullable = false)
var userId: JLong = _
@BeanProperty
@Column(name = "city_id", nullable = false)
var cityId: JLong = _
@BeanProperty
@Column(nullable = false)
var time: ZonedDateTime = _
}
object ClinicHistory {
def apply(userId: Long, id: Long, name: String, cityId: Long, time: ZonedDateTime): ClinicHistory = {
val clinic = new ClinicHistory
clinic.userId = userId
clinic.id = id
clinic.name = name
clinic.time = time
clinic.cityId = cityId
clinic
}
}

Some files were not shown because too many files have changed in this diff Show More