From 65b9e53b66e162f1a97545c05d5a09048adb7557 Mon Sep 17 00:00:00 2001 From: Evgeniy Alekseev Date: Sun, 17 Nov 2019 16:23:43 +0300 Subject: [PATCH] additional methods to types endpoint --- src/main/resources/reference.conf | 6 +++ .../arcanis/ffxivbis/http/RootEndpoint.scala | 2 +- .../http/api/v1/RootApiV1Endpoint.scala | 5 +- .../ffxivbis/http/api/v1/TypesEndpoint.scala | 28 +++++++++-- .../me/arcanis/ffxivbis/models/Party.scala | 6 +-- .../ffxivbis/service/RateLimiter.scala | 47 +++++++++++++++++++ .../http/api/v1/TypesEndpointTest.scala | 15 +++++- 7 files changed, 98 insertions(+), 11 deletions(-) create mode 100644 src/main/scala/me/arcanis/ffxivbis/service/RateLimiter.scala diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf index 9fb34f5..27e30c7 100644 --- a/src/main/resources/reference.conf +++ b/src/main/resources/reference.conf @@ -54,5 +54,11 @@ me.arcanis.ffxivbis { host = "127.0.0.1" # port to bind, int, required port = 8000 + + # rate limits + limits { + intetval = 1m + max-count = 60 + } } } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/RootEndpoint.scala b/src/main/scala/me/arcanis/ffxivbis/http/RootEndpoint.scala index 23e9e55..dba534f 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/RootEndpoint.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/RootEndpoint.scala @@ -27,7 +27,7 @@ class RootEndpoint(system: ActorSystem, storage: ActorRef, ariyala: ActorRef) implicit val timeout: Timeout = config.getDuration("me.arcanis.ffxivbis.settings.request-timeout") - private val rootApiV1Endpoint: RootApiV1Endpoint = new RootApiV1Endpoint(storage, ariyala) + private val rootApiV1Endpoint: RootApiV1Endpoint = new RootApiV1Endpoint(storage, ariyala, config) private val rootView: RootView = new RootView(storage, ariyala) private val httpLogger = Logger("http") diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/RootApiV1Endpoint.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/RootApiV1Endpoint.scala index 24ad6e5..7259d7c 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/RootApiV1Endpoint.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/RootApiV1Endpoint.scala @@ -12,16 +12,17 @@ import akka.actor.ActorRef import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route import akka.util.Timeout +import com.typesafe.config.Config import me.arcanis.ffxivbis.http.api.v1.json.JsonSupport -class RootApiV1Endpoint(storage: ActorRef, ariyala: ActorRef) +class RootApiV1Endpoint(storage: ActorRef, ariyala: ActorRef, config: Config) (implicit timeout: Timeout) extends JsonSupport with HttpHandler { private val biSEndpoint = new BiSEndpoint(storage, ariyala) private val lootEndpoint = new LootEndpoint(storage) private val playerEndpoint = new PlayerEndpoint(storage, ariyala) - private val typesEndpoint = new TypesEndpoint + private val typesEndpoint = new TypesEndpoint(config) private val userEndpoint = new UserEndpoint(storage) def route: Route = diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/TypesEndpoint.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/TypesEndpoint.scala index 88d6f86..87c350e 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/TypesEndpoint.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/TypesEndpoint.scala @@ -10,17 +10,18 @@ package me.arcanis.ffxivbis.http.api.v1 import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server._ +import com.typesafe.config.Config import io.swagger.v3.oas.annotations.media.{ArraySchema, Content, Schema} import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.Operation import javax.ws.rs._ import me.arcanis.ffxivbis.http.api.v1.json._ -import me.arcanis.ffxivbis.models.{Job, Permission, Piece} +import me.arcanis.ffxivbis.models.{Job, Party, Permission, Piece} @Path("api/v1") -class TypesEndpoint extends JsonSupport { +class TypesEndpoint(config: Config) extends JsonSupport { - def route: Route = getJobs ~ getPermissions ~ getPieces + def route: Route = getJobs ~ getPermissions ~ getPieces ~ getPriority @GET @Path("types/jobs") @@ -84,4 +85,25 @@ class TypesEndpoint extends JsonSupport { complete(Piece.available) } } + + @GET + @Path("types/priority") + @Produces(value = Array("application/json")) + @Operation(summary = "priority list", description = "Returns the current priority list", + responses = Array( + new ApiResponse(responseCode = "200", description = "Priority order", + content = Array(new Content( + array = new ArraySchema(schema = new Schema(implementation = classOf[String])) + ))), + new ApiResponse(responseCode = "500", description = "Internal server error", + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), + ), + tags = Array("types"), + ) + def getPriority: Route = + path("types" / "priority") { + get { + complete(Party.getRules(config)) + } + } } diff --git a/src/main/scala/me/arcanis/ffxivbis/models/Party.scala b/src/main/scala/me/arcanis/ffxivbis/models/Party.scala index b71a1e1..41d58d2 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/Party.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/Party.scala @@ -36,9 +36,6 @@ case class Party(partyId: String, rules: Seq[String], players: Map[PlayerId, Pla } object Party { - private def getRules(config: Config): Seq[String] = - config.getStringList("me.arcanis.ffxivbis.settings.priority").asScala.toSeq - def apply(partyId: Option[String], config: Config): Party = new Party(partyId.getOrElse(randomPartyId), getRules(config), Map.empty) @@ -55,5 +52,8 @@ object Party { Party(partyId, getRules(config), playersWithItems) } + def getRules(config: Config): Seq[String] = + config.getStringList("me.arcanis.ffxivbis.settings.priority").asScala.toSeq + def randomPartyId: String = Random.alphanumeric.take(20).mkString } diff --git a/src/main/scala/me/arcanis/ffxivbis/service/RateLimiter.scala b/src/main/scala/me/arcanis/ffxivbis/service/RateLimiter.scala new file mode 100644 index 0000000..48525ee --- /dev/null +++ b/src/main/scala/me/arcanis/ffxivbis/service/RateLimiter.scala @@ -0,0 +1,47 @@ +package me.arcanis.ffxivbis.service + +import java.time.Instant + +import akka.actor.Actor + +import scala.concurrent.ExecutionContext +import scala.concurrent.duration.FiniteDuration + +class RateLimiter extends Actor { + import RateLimiter._ + import me.arcanis.ffxivbis.utils.Implicits._ + + implicit private val executionContext: ExecutionContext = context.system.dispatcher + + private val maxRequestCount: Int = context.system.settings.config.getInt("me.arcanis.ffxivbis.web.limits.max-count") + private val requestInterval: FiniteDuration = context.system.settings.config.getDuration("me.arcanis.ffxivbis.web.limits.interval") + + override def receive: Receive = handle(Map.empty) + + private def handle(cache: Map[String, Usage]): Receive = { + case username: String => + val client = sender() + val usage = if (cache.contains(username)) { + cache(username) + } else { + context.system.scheduler.scheduleOnce(requestInterval, self, Reset(username)) + Usage() + } + context become handle(cache + (username -> usage.increment)) + + val response = if (usage.count > maxRequestCount) Some(usage.left) else None + client ! response + + case Reset(username) => + context become handle(cache - username) + } +} + +object RateLimiter { + private case class Usage(count: Int = 0, since: Instant = Instant.now) { + def increment: Usage = copy(count = count + 1) + def left: Long = (Instant.now.toEpochMilli - since.toEpochMilli) / 1000 + } + + case class Reset(username: String) +} \ No newline at end of file diff --git a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/TypesEndpointTest.scala b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/TypesEndpointTest.scala index 3b46c96..859c87a 100644 --- a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/TypesEndpointTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/TypesEndpointTest.scala @@ -3,8 +3,10 @@ package me.arcanis.ffxivbis.http.api.v1 import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.testkit.ScalatestRouteTest import akka.http.scaladsl.server._ +import com.typesafe.config.Config +import me.arcanis.ffxivbis.Settings import me.arcanis.ffxivbis.http.api.v1.json._ -import me.arcanis.ffxivbis.models.{Job, Permission, Piece} +import me.arcanis.ffxivbis.models.{Job, Party, Permission, Piece} import org.scalatest.{Matchers, WordSpec} import scala.language.postfixOps @@ -12,7 +14,9 @@ import scala.language.postfixOps class TypesEndpointTest extends WordSpec with Matchers with ScalatestRouteTest with JsonSupport { - private val route: Route = new TypesEndpoint().route + private val route: Route = new TypesEndpoint(testConfig).route + + override def testConfig: Config = Settings.withRandomDatabase "api v1 types endpoint" must { @@ -37,5 +41,12 @@ class TypesEndpointTest extends WordSpec } } + "return current priority" in { + Get("/types/priority") ~> route ~> check { + status shouldEqual StatusCodes.OK + responseAs[Seq[String]] shouldEqual Party.getRules(testConfig) + } + } + } }