diff --git a/libraries.sbt b/libraries.sbt index 1e87d66..d1d2463 100644 --- a/libraries.sbt +++ b/libraries.sbt @@ -1,26 +1,26 @@ -val AkkaVersion = "2.6.18" -val AkkaHttpVersion = "10.2.7" -val ScalaTestVersion = "3.2.10" +val AkkaVersion = "2.6.19" +val AkkaHttpVersion = "10.2.9" +val ScalaTestVersion = "3.2.12" val SlickVersion = "3.3.3" -libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.10" -libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.4" +libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.11" +libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5" libraryDependencies += "com.typesafe.akka" %% "akka-http" % AkkaHttpVersion libraryDependencies += "com.typesafe.akka" %% "akka-http-spray-json" % AkkaHttpVersion libraryDependencies += "com.typesafe.akka" %% "akka-actor-typed" % AkkaVersion libraryDependencies += "com.typesafe.akka" %% "akka-stream" % AkkaVersion -libraryDependencies += "com.github.swagger-akka-http" %% "swagger-akka-http" % "2.6.0" +libraryDependencies += "com.github.swagger-akka-http" %% "swagger-akka-http" % "2.7.0" libraryDependencies += "jakarta.platform" % "jakarta.jakartaee-web-api" % "9.1.0" -libraryDependencies += "ch.megard" %% "akka-http-cors" % "1.1.2" +libraryDependencies += "ch.megard" %% "akka-http-cors" % "1.1.3" libraryDependencies += "io.spray" %% "spray-json" % "1.3.6" libraryDependencies += "org.playframework.anorm" %% "anorm" % "2.6.10" libraryDependencies += "com.zaxxer" % "HikariCP" % "5.0.1" exclude("org.slf4j", "slf4j-api") -libraryDependencies += "org.flywaydb" % "flyway-core" % "8.4.1" +libraryDependencies += "org.flywaydb" % "flyway-core" % "8.5.12" libraryDependencies += "org.xerial" % "sqlite-jdbc" % "3.36.0.3" -libraryDependencies += "org.postgresql" % "postgresql" % "42.3.1" +libraryDependencies += "org.postgresql" % "postgresql" % "42.3.6" libraryDependencies += "org.mindrot" % "jbcrypt" % "0.4" libraryDependencies += "com.google.guava" % "guava" % "31.0.1-jre" diff --git a/src/main/resources/db/migration/postgresql/V8_0__Remove_special_characters.sql b/src/main/resources/db/migration/postgresql/V8_0__Remove_special_characters.sql new file mode 100644 index 0000000..6535f12 --- /dev/null +++ b/src/main/resources/db/migration/postgresql/V8_0__Remove_special_characters.sql @@ -0,0 +1,3 @@ +update parties set party_alias = regexp_replace(party_alias, '[^A-Za-z0-9!@#$%^&*()\-_=+;:'',./? ]', '', 'g'); +update players set nick = regexp_replace(nick, '[^A-Za-z0-9!@#$%^&*()\-_=+;:'',./? ]', '', 'g'); +update users set username = regexp_replace(username, '[^A-Za-z0-9!@#$%^&*()\-_=+;:'',./? ]', '', 'g'); \ No newline at end of file diff --git a/src/main/resources/html/bis.html b/src/main/resources/html/bis.html index 1013e71..976f414 100644 --- a/src/main/resources/html/bis.html +++ b/src/main/resources/html/bis.html @@ -9,9 +9,9 @@ - + - + @@ -157,11 +157,11 @@ - + - + - + diff --git a/src/main/resources/html/index.html b/src/main/resources/html/index.html index 9653836..14664bd 100644 --- a/src/main/resources/html/index.html +++ b/src/main/resources/html/index.html @@ -87,6 +87,7 @@ + - + - + - + diff --git a/src/main/resources/html/party.html b/src/main/resources/html/party.html index a5cbecd..0c5719f 100644 --- a/src/main/resources/html/party.html +++ b/src/main/resources/html/party.html @@ -9,9 +9,9 @@ - + - + @@ -147,11 +147,11 @@ - + - + - + diff --git a/src/main/resources/html/users.html b/src/main/resources/html/users.html index ac930f8..2f2c33c 100644 --- a/src/main/resources/html/users.html +++ b/src/main/resources/html/users.html @@ -9,9 +9,9 @@ - + - + @@ -141,11 +141,11 @@ - + - + - + diff --git a/src/main/scala/me/arcanis/ffxivbis/http/ValidatorHelper.scala b/src/main/scala/me/arcanis/ffxivbis/http/ValidatorHelper.scala new file mode 100644 index 0000000..d9d4f5f --- /dev/null +++ b/src/main/scala/me/arcanis/ffxivbis/http/ValidatorHelper.scala @@ -0,0 +1,16 @@ +package me.arcanis.ffxivbis.http + +import scala.collection.immutable.HashSet + +trait ValidatorHelper { + + def isValidString(string: String): Boolean = string.nonEmpty && string.forall(isValidSymbol) + + def isValidSymbol(char: Char): Boolean = + char.isLetterOrDigit || ValidatorHelper.VALID_CHARACTERS.contains(char) +} + +object ValidatorHelper { + + final val VALID_CHARACTERS = HashSet.from("!@#$%^&*()-_=+;:',./? ") +} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpoint.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpoint.scala index e2f6f81..a725cc9 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpoint.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpoint.scala @@ -49,7 +49,12 @@ class BiSEndpoint( summary = "create best in slot", description = "Create the best in slot set", parameters = Array( - new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), + new Parameter( + name = "partyId", + in = ParameterIn.PATH, + description = "unique party ID", + example = "o3KicHQPW5b0JcOm5yI3" + ), ), requestBody = new RequestBody( description = "player best in slot description", @@ -105,7 +110,12 @@ class BiSEndpoint( summary = "get best in slot", description = "Return the best in slot items", parameters = Array( - new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), + new Parameter( + name = "partyId", + in = ParameterIn.PATH, + description = "unique party ID", + example = "o3KicHQPW5b0JcOm5yI3" + ), new Parameter( name = "nick", in = ParameterIn.QUERY, @@ -167,7 +177,12 @@ class BiSEndpoint( summary = "modify best in slot", description = "Add or remove an item from the best in slot", parameters = Array( - new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), + new Parameter( + name = "partyId", + in = ParameterIn.PATH, + description = "unique party ID", + example = "o3KicHQPW5b0JcOm5yI3" + ), ), requestBody = new RequestBody( description = "action and piece description", diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpoint.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpoint.scala index c1bdbce..75cb5f5 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpoint.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpoint.scala @@ -46,7 +46,12 @@ class LootEndpoint(override val storage: ActorRef[Message], override val auth: A summary = "get loot list", description = "Return the looted items", parameters = Array( - new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), + new Parameter( + name = "partyId", + in = ParameterIn.PATH, + description = "unique party ID", + example = "o3KicHQPW5b0JcOm5yI3" + ), new Parameter( name = "nick", in = ParameterIn.QUERY, @@ -107,7 +112,12 @@ class LootEndpoint(override val storage: ActorRef[Message], override val auth: A summary = "modify loot list", description = "Add or remove an item from the loot list", parameters = Array( - new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), + new Parameter( + name = "partyId", + in = ParameterIn.PATH, + description = "unique party ID", + example = "o3KicHQPW5b0JcOm5yI3" + ), ), requestBody = new RequestBody( description = "action and piece description", @@ -164,7 +174,12 @@ class LootEndpoint(override val storage: ActorRef[Message], override val auth: A summary = "suggest loot", description = "Suggest loot piece to party", parameters = Array( - new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), + new Parameter( + name = "partyId", + in = ParameterIn.PATH, + description = "unique party ID", + example = "o3KicHQPW5b0JcOm5yI3" + ), ), requestBody = new RequestBody( description = "piece description", diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/PartyEndpoint.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/PartyEndpoint.scala index 6f7f86f..023c316 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/PartyEndpoint.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/PartyEndpoint.scala @@ -49,7 +49,12 @@ class PartyEndpoint( summary = "get party description", description = "Return the party description", parameters = Array( - new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), + new Parameter( + name = "partyId", + in = ParameterIn.PATH, + description = "unique party ID", + example = "o3KicHQPW5b0JcOm5yI3" + ), ), responses = Array( new ApiResponse( @@ -96,7 +101,12 @@ class PartyEndpoint( summary = "modify party description", description = "Edit party description", parameters = Array( - new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), + new Parameter( + name = "partyId", + in = ParameterIn.PATH, + description = "unique party ID", + example = "o3KicHQPW5b0JcOm5yI3" + ), ), requestBody = new RequestBody( description = "new party description", diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/PlayerEndpoint.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/PlayerEndpoint.scala index b2d37d0..5b9849e 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/PlayerEndpoint.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/PlayerEndpoint.scala @@ -50,7 +50,12 @@ class PlayerEndpoint( summary = "get party", description = "Return the players who belong to the party", parameters = Array( - new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), + new Parameter( + name = "partyId", + in = ParameterIn.PATH, + description = "unique party ID", + example = "o3KicHQPW5b0JcOm5yI3" + ), new Parameter( name = "nick", in = ParameterIn.QUERY, @@ -111,7 +116,12 @@ class PlayerEndpoint( summary = "get party statistics", description = "Return the party statistics", parameters = Array( - new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), + new Parameter( + name = "partyId", + in = ParameterIn.PATH, + description = "unique party ID", + example = "o3KicHQPW5b0JcOm5yI3" + ), new Parameter( name = "nick", in = ParameterIn.QUERY, @@ -172,7 +182,12 @@ class PlayerEndpoint( summary = "modify party", description = "Add or remove a player from party list", parameters = Array( - new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), + new Parameter( + name = "partyId", + in = ParameterIn.PATH, + description = "unique party ID", + example = "o3KicHQPW5b0JcOm5yI3" + ), ), requestBody = new RequestBody( description = "player description", diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/UserEndpoint.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/UserEndpoint.scala index 8b05666..6ce3011 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/UserEndpoint.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/UserEndpoint.scala @@ -96,7 +96,12 @@ class UserEndpoint(override val storage: ActorRef[Message], override val auth: A summary = "create new user", description = "Add an user to the specified party", parameters = Array( - new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), + new Parameter( + name = "partyId", + in = ParameterIn.PATH, + description = "unique party ID", + example = "o3KicHQPW5b0JcOm5yI3" + ), ), requestBody = new RequestBody( description = "user description", @@ -151,7 +156,12 @@ class UserEndpoint(override val storage: ActorRef[Message], override val auth: A summary = "remove user", description = "Remove an user from the specified party", parameters = Array( - new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), + new Parameter( + name = "partyId", + in = ParameterIn.PATH, + description = "unique party ID", + example = "o3KicHQPW5b0JcOm5yI3" + ), new Parameter(name = "username", in = ParameterIn.PATH, description = "username to remove", example = "siuan"), ), responses = Array( @@ -195,7 +205,12 @@ class UserEndpoint(override val storage: ActorRef[Message], override val auth: A summary = "get users", description = "Return the list of users belong to party", parameters = Array( - new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), + new Parameter( + name = "partyId", + in = ParameterIn.PATH, + description = "unique party ID", + example = "o3KicHQPW5b0JcOm5yI3" + ), ), responses = Array( new ApiResponse( @@ -246,7 +261,12 @@ class UserEndpoint(override val storage: ActorRef[Message], override val auth: A summary = "get current user", description = "Return the current user descriptor", parameters = Array( - new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), + new Parameter( + name = "partyId", + in = ParameterIn.PATH, + description = "unique party ID", + example = "o3KicHQPW5b0JcOm5yI3" + ), ), responses = Array( new ApiResponse( diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyDescriptionModel.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyDescriptionModel.scala index bfb5b2b..26dd085 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyDescriptionModel.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyDescriptionModel.scala @@ -12,9 +12,11 @@ import io.swagger.v3.oas.annotations.media.Schema import me.arcanis.ffxivbis.models.PartyDescription case class PartyDescriptionModel( - @Schema(description = "party id", required = true, example = "abcdefgh") partyId: String, + @Schema(description = "party id", required = true, example = "o3KicHQPW5b0JcOm5yI3") partyId: String, @Schema(description = "party name") partyAlias: Option[String] -) { +) extends Validator { + + require(partyAlias.forall(isValidString), stringMatchError("Party alias")) def toDescription: PartyDescription = PartyDescription(partyId, partyAlias) } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyIdModel.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyIdModel.scala index 88fc028..7d5a435 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyIdModel.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyIdModel.scala @@ -10,4 +10,6 @@ package me.arcanis.ffxivbis.http.api.v1.json import io.swagger.v3.oas.annotations.media.Schema -case class PartyIdModel(@Schema(description = "party id", required = true, example = "abcdefgh") partyId: String) +case class PartyIdModel( + @Schema(description = "party id", required = true, example = "o3KicHQPW5b0JcOm5yI3") partyId: String +) diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerBiSLinkModel.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerBiSLinkModel.scala index 7dfc244..e62f366 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerBiSLinkModel.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerBiSLinkModel.scala @@ -17,4 +17,7 @@ case class PlayerBiSLinkModel( example = "https://ffxiv.ariyala.com/19V5R" ) link: String, @Schema(description = "player description", required = true) playerId: PlayerIdModel -) +) extends Validator { + + require(isValidString(link), stringMatchError("BiS link")) +} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdModel.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdModel.scala index efca7e6..8ee4b13 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdModel.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdModel.scala @@ -12,10 +12,14 @@ import io.swagger.v3.oas.annotations.media.Schema import me.arcanis.ffxivbis.models.{Job, PlayerId} case class PlayerIdModel( - @Schema(description = "unique party ID. Required in responses", example = "abcdefgh") partyId: Option[String], + @Schema(description = "unique party ID. Required in responses", example = "o3KicHQPW5b0JcOm5yI3") partyId: Option[ + String + ], @Schema(description = "job name", required = true, example = "DNC") job: String, @Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String -) { +) extends Validator { + + require(isValidString(nick), stringMatchError("Player name")) def withPartyId(partyId: String): PlayerId = PlayerId(partyId, Job.withName(job), nick) diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdWithCountersModel.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdWithCountersModel.scala index df72789..1623ce7 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdWithCountersModel.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdWithCountersModel.scala @@ -12,7 +12,7 @@ import io.swagger.v3.oas.annotations.media.Schema import me.arcanis.ffxivbis.models.PlayerIdWithCounters case class PlayerIdWithCountersModel( - @Schema(description = "unique party ID", required = true, example = "abcdefgh") partyId: String, + @Schema(description = "unique party ID", required = true, example = "o3KicHQPW5b0JcOm5yI3") partyId: String, @Schema(description = "job name", required = true, example = "DNC") job: String, @Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String, @Schema(description = "is piece required by player or not", required = true) isRequired: Boolean, diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerModel.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerModel.scala index 515d17d..2d55972 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerModel.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerModel.scala @@ -12,7 +12,7 @@ import io.swagger.v3.oas.annotations.media.Schema import me.arcanis.ffxivbis.models.{BiS, Job, Player} case class PlayerModel( - @Schema(description = "unique party ID", required = true, example = "abcdefgh") partyId: String, + @Schema(description = "unique party ID", required = true, example = "o3KicHQPW5b0JcOm5yI3") partyId: String, @Schema(description = "job name", required = true, example = "DNC") job: String, @Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String, @Schema(description = "pieces in best in slot") bis: Option[Seq[PieceModel]], @@ -24,7 +24,10 @@ case class PlayerModel( `type` = "number" ) lootCountBiS: Option[Int], @Schema(description = "total count of looted pieces", `type` = "number") lootCountTotal: Option[Int], -) { +) extends Validator { + + require(isValidString(nick), stringMatchError("Player name")) + require(link.forall(isValidString), stringMatchError("BiS link")) def toPlayer: Player = Player( diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/UserModel.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/UserModel.scala index 437d940..19306f2 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/UserModel.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/UserModel.scala @@ -12,23 +12,30 @@ import io.swagger.v3.oas.annotations.media.Schema import me.arcanis.ffxivbis.models.{Permission, User} case class UserModel( - @Schema(description = "unique party ID", required = true, example = "abcdefgh") partyId: String, + @Schema(description = "unique party ID", required = true, example = "o3KicHQPW5b0JcOm5yI3") partyId: String, @Schema(description = "username to login to party", required = true, example = "siuan") username: String, - @Schema(description = "password to login to party", required = true, example = "pa55w0rd") password: String, + @Schema(description = "password to login to party, required for user editing", example = "pa55w0rd") password: Option[ + String + ], @Schema( description = "user permission", defaultValue = "get", `type` = "string", allowableValues = Array("get", "post", "admin") ) permission: Option[Permission.Value] = None -) { +) extends Validator { + + require(isValidString(username), stringMatchError("Username")) + require(password.forall(_.nonEmpty), "Password must not be empty") def toUser: User = - User(partyId, username, password, permission.getOrElse(Permission.get)) + password.fold(throw new IllegalArgumentException("Password must noot be empty"))( + User(partyId, username, _, permission.getOrElse(Permission.get)) + ) } object UserModel { def fromUser(user: User): UserModel = - UserModel(user.partyId, user.username, "", Some(user.permission)) + UserModel(user.partyId, user.username, None, Some(user.permission)) } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/Validator.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/Validator.scala new file mode 100644 index 0000000..fa68852 --- /dev/null +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/Validator.scala @@ -0,0 +1,9 @@ +package me.arcanis.ffxivbis.http.api.v1.json + +import me.arcanis.ffxivbis.http.ValidatorHelper + +trait Validator extends ValidatorHelper { + + def stringMatchError(what: String): String = + s"$what must contain only letters or digits or one of (${ValidatorHelper.VALID_CHARACTERS.mkString(", ")})" +} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/helpers/BiSHelper.scala b/src/main/scala/me/arcanis/ffxivbis/http/helpers/BiSHelper.scala index 0cdcd1d..c4c2188 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/helpers/BiSHelper.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/helpers/BiSHelper.scala @@ -45,13 +45,15 @@ trait BiSHelper extends BisProviderHelper { timeout: Timeout, scheduler: Scheduler ): Future[Unit] = - storage.ask(RemovePiecesFromBiS(playerId, _)).flatMap { _ => - downloadBiS(link, playerId.job) - .flatMap { bis => - Future.traverse(bis.pieces)(addPieceBiS(playerId, _)) - } - .map(_ => ()) - } + storage + .ask(RemovePiecesFromBiS(playerId, _)) + .flatMap { _ => + downloadBiS(link, playerId.job) + .flatMap { bis => + Future.traverse(bis.pieces)(addPieceBiS(playerId, _)) + } + } + .flatMap(_ => storage.ask(UpdateBiSLink(playerId, link, _))) def removePieceBiS(playerId: PlayerId, piece: Piece)(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] = storage.ask(RemovePieceFromBiS(playerId, piece, _)) diff --git a/src/main/scala/me/arcanis/ffxivbis/http/helpers/PlayerHelper.scala b/src/main/scala/me/arcanis/ffxivbis/http/helpers/PlayerHelper.scala index a19ad30..1047eae 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/helpers/PlayerHelper.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/helpers/PlayerHelper.scala @@ -27,15 +27,15 @@ trait PlayerHelper extends BisProviderHelper { )(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Unit] = storage .ask(ref => AddPlayer(player, ref)) - .map { res => + .map { _ => player.link.map(_.trim).filter(_.nonEmpty) match { case Some(link) => downloadBiS(link, player.job) .map { bis => bis.pieces.map(piece => storage.ask(AddPieceToBis(player.playerId, piece, _))) } - .map(_ => res) - case None => Future.successful(res) + .flatMap(_ => storage.ask(UpdateBiSLink(player.playerId, link, _))) + case None => Future.successful(()) } } .flatten diff --git a/src/main/scala/me/arcanis/ffxivbis/messages/DatabaseMessage.scala b/src/main/scala/me/arcanis/ffxivbis/messages/DatabaseMessage.scala index 33b5aeb..3f291c2 100644 --- a/src/main/scala/me/arcanis/ffxivbis/messages/DatabaseMessage.scala +++ b/src/main/scala/me/arcanis/ffxivbis/messages/DatabaseMessage.scala @@ -95,6 +95,11 @@ object DatabaseMessage { override val isReadOnly: Boolean = false } + case class UpdateBiSLink(playerId: PlayerId, link: String, actorRef: ActorRef[Unit]) extends PartyDatabaseMessage { + override val partyId: String = playerId.partyId + override val isReadOnly: Boolean = false + } + case class UpdateParty(partyDescription: PartyDescription, replyTo: ActorRef[Unit]) extends PartyDatabaseMessage { override val partyId: String = partyDescription.partyId override val isReadOnly: Boolean = false diff --git a/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabasePartyHandler.scala b/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabasePartyHandler.scala index 2e679bd..07afc0a 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabasePartyHandler.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabasePartyHandler.scala @@ -62,6 +62,10 @@ trait DatabasePartyHandler { this: Database => run(profile.deletePlayer(playerId))(_ => client ! ()) Behaviors.same + case UpdateBiSLink(playerId, link, client) => + run(profile.updateBiSLink(playerId, link))(_ => client ! ()) + Behaviors.same + case UpdateParty(description, client) => run(profile.insertPartyDescription(description))(_ => client ! ()) Behaviors.same diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala index 20c6676..7663c81 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala @@ -53,12 +53,14 @@ trait BiSProfile extends DatabaseConnection { def getPiecesBiSById(playerId: Long): Future[Seq[Loot]] = getPiecesBiSById(Seq(playerId)) def getPiecesBiSById(playerIds: Seq[Long]): Future[Seq[Loot]] = - withConnection { implicit conn => - SQL("""select * from bis where player_id in ({player_ids})""") - .on("player_ids" -> playerIds) - .executeQuery() - .as(loot.*) - } + if (playerIds.isEmpty) Future.successful(Seq.empty) + else + withConnection { implicit conn => + SQL("""select * from bis where player_id in ({player_ids})""") + .on("player_ids" -> playerIds) + .executeQuery() + .as(loot.*) + } def insertPieceBiSById(piece: Piece)(playerId: Long): Future[Int] = withConnection { implicit conn => diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/LootProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/LootProfile.scala index a873faa..4c8116d 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/LootProfile.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/LootProfile.scala @@ -59,12 +59,14 @@ trait LootProfile extends DatabaseConnection { def getPiecesById(playerId: Long): Future[Seq[Loot]] = getPiecesById(Seq(playerId)) def getPiecesById(playerIds: Seq[Long]): Future[Seq[Loot]] = - withConnection { implicit conn => - SQL("""select * from loot where player_id in ({player_ids})""") - .on("player_ids" -> playerIds) - .executeQuery() - .as(loot.*) - } + if (playerIds.isEmpty) Future.successful(Seq.empty) + else + withConnection { implicit conn => + SQL("""select * from loot where player_id in ({player_ids})""") + .on("player_ids" -> playerIds) + .executeQuery() + .as(loot.*) + } def insertPieceById(loot: Loot)(playerId: Long): Future[Int] = withConnection { implicit conn => diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/PlayersProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/PlayersProfile.scala index 4b0758c..4921fed 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/PlayersProfile.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/PlayersProfile.scala @@ -101,4 +101,18 @@ trait PlayersProfile extends DatabaseConnection { .executeUpdate() } + def updateBiSLink(playerId: PlayerId, link: String): Future[Int] = + withConnection { implicit conn => + SQL("""update players + | set bis_link = {link} + | where party_id = {party_id} and nick = {nick} and job = {job}""".stripMargin) + .on( + "link" -> link, + "party_id" -> playerId.partyId, + "nick" -> playerId.nick, + "job" -> playerId.job.toString + ) + .executeUpdate() + } + } diff --git a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/UserEndpointTest.scala b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/UserEndpointTest.scala index b00c6e1..2fe7ae5 100644 --- a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/UserEndpointTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/UserEndpointTest.scala @@ -48,7 +48,7 @@ class UserEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute "create a party" in { val uri = Uri(s"/party") - val entity = UserModel.fromUser(Fixtures.userAdmin).copy(password = Fixtures.userPassword) + val entity = UserModel.fromUser(Fixtures.userAdmin).copy(password = Some(Fixtures.userPassword)) Post(uri, entity) ~> route ~> check { status shouldEqual StatusCodes.OK @@ -57,7 +57,7 @@ class UserEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute } "add user" in { - val entity = UserModel.fromUser(Fixtures.userGet).copy(partyId = partyId, password = Fixtures.userPassword2) + val entity = UserModel.fromUser(Fixtures.userGet).copy(partyId = partyId, password = Some(Fixtures.userPassword2)) Post(endpoint, entity).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.Accepted diff --git a/src/test/scala/me/arcanis/ffxivbis/service/database/DatabasePartyHandlerTest.scala b/src/test/scala/me/arcanis/ffxivbis/service/database/DatabasePartyHandlerTest.scala index aac8d1f..9fdf97e 100644 --- a/src/test/scala/me/arcanis/ffxivbis/service/database/DatabasePartyHandlerTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/service/database/DatabasePartyHandlerTest.scala @@ -66,6 +66,18 @@ class DatabasePartyHandlerTest extends ScalaTestWithActorTestKit(Settings.withRa Compare.seqEquals(party.getPlayers, Seq(newPlayer)) shouldEqual true } + "update bis link" in { + val updateProbe = testKit.createTestProbe[Unit]() + val newPlayer = Fixtures.playerEmpty.copy(priority = 2, link = Some("link")) + + database ! UpdateBiSLink(Fixtures.playerEmpty.playerId, "link", updateProbe.ref) + updateProbe.expectMessage(askTimeout, ()) + + val probe = testKit.createTestProbe[Option[Player]]() + database ! GetPlayer(Fixtures.playerEmpty.playerId, probe.ref) + probe.expectMessage(askTimeout, Some(newPlayer)) + } + "remove player" in { val updateProbe = testKit.createTestProbe[Unit]() database ! RemovePlayer(Fixtures.playerEmpty.playerId, updateProbe.ref)