From eeb5178efc91f6dfbc0858986d7b576292a3cc9d Mon Sep 17 00:00:00 2001 From: Evgenii Alekseev Date: Sat, 15 Jan 2022 23:15:24 +0300 Subject: [PATCH] migrate to bootstrap (#14) --- .github/workflows/release.yml | 4 +- .github/workflows/run-tests.yml | 4 +- Makefile | 35 ++ libraries.sbt | 3 +- src/main/resources/html/bis.html | 349 ++++++++++++++++++ src/main/resources/html/index.html | 183 +++++++++ src/main/resources/html/loot.html | 338 +++++++++++++++++ src/main/resources/html/party.html | 258 +++++++++++++ src/main/resources/html/redoc.html | 19 + src/main/resources/html/swagger.html | 24 -- src/main/resources/html/users.html | 230 ++++++++++++ src/main/resources/reference.conf | 109 +++--- src/main/resources/static/favicon.ico | Bin 0 -> 30506 bytes src/main/resources/static/load.js | 54 +++ src/main/resources/static/styles.css | 277 -------------- src/main/resources/static/table_export.js | 31 -- src/main/resources/static/table_search.js | 21 -- src/main/resources/static/utils.js | 44 +++ .../resources/swagger-info/description.md | 2 + .../me/arcanis/ffxivbis/Application.scala | 6 +- .../me/arcanis/ffxivbis/Configuration.scala | 23 ++ .../scala/me/arcanis/ffxivbis/ffxivbis.scala | 9 +- .../arcanis/ffxivbis/http/Authorization.scala | 46 +-- .../ffxivbis/http/AuthorizationProvider.scala | 51 +++ .../me/arcanis/ffxivbis/http/HttpLog.scala | 74 ++++ .../arcanis/ffxivbis/http/RootEndpoint.scala | 36 +- .../me/arcanis/ffxivbis/http/Swagger.scala | 2 +- .../ffxivbis/http/api/v1/BiSEndpoint.scala | 58 +-- .../ffxivbis/http/api/v1/HttpHandler.scala | 8 +- .../ffxivbis/http/api/v1/LootEndpoint.scala | 62 ++-- .../ffxivbis/http/api/v1/PartyEndpoint.scala | 42 ++- .../ffxivbis/http/api/v1/PlayerEndpoint.scala | 109 ++++-- .../http/api/v1/RootApiV1Endpoint.scala | 20 +- .../ffxivbis/http/api/v1/TypesEndpoint.scala | 51 ++- .../ffxivbis/http/api/v1/UserEndpoint.scala | 130 ++++--- .../ffxivbis/http/api/v1/json/ApiAction.scala | 2 +- ...PartyIdResponse.scala => ErrorModel.scala} | 4 +- .../http/api/v1/json/JsonSupport.scala | 34 +- .../{LootResponse.scala => LootModel.scala} | 22 +- ...onse.scala => PartyDescriptionModel.scala} | 12 +- ...ErrorResponse.scala => PartyIdModel.scala} | 4 +- ...nResponse.scala => PieceActionModel.scala} | 10 +- .../{PieceResponse.scala => PieceModel.scala} | 12 +- ...Response.scala => PlayerActionModel.scala} | 6 +- ...esponse.scala => PlayerBiSLinkModel.scala} | 6 +- ...erIdResponse.scala => PlayerIdModel.scala} | 10 +- ....scala => PlayerIdWithCountersModel.scala} | 12 +- ...PlayerResponse.scala => PlayerModel.scala} | 26 +- .../{UserResponse.scala => UserModel.scala} | 11 +- .../http/{ => helpers}/BiSHelper.scala | 12 +- .../{ => helpers}/BisProviderHelper.scala | 12 +- .../http/{ => helpers}/LootHelper.scala | 12 +- .../http/{ => helpers}/PlayerHelper.scala | 14 +- .../http/{ => helpers}/UserHelper.scala | 12 +- .../ffxivbis/http/view/BasePartyView.scala | 73 ---- .../arcanis/ffxivbis/http/view/BiSView.scala | 173 --------- .../ffxivbis/http/view/ErrorView.scala | 20 - .../ffxivbis/http/view/ExportToCSVView.scala | 21 -- .../ffxivbis/http/view/IndexView.scala | 104 ------ .../ffxivbis/http/view/LootSuggestView.scala | 163 -------- .../arcanis/ffxivbis/http/view/LootView.scala | 164 -------- .../ffxivbis/http/view/PlayerView.scala | 152 -------- .../arcanis/ffxivbis/http/view/RootView.scala | 89 +++-- .../ffxivbis/http/view/SearchLineView.scala | 26 -- .../arcanis/ffxivbis/http/view/UserView.scala | 149 -------- .../messages/BiSProviderMessage.scala | 8 + .../ffxivbis/messages/ContolMessage.scala | 8 + .../ffxivbis/messages/DatabaseMessage.scala | 58 ++- .../arcanis/ffxivbis/messages/Message.scala | 9 + .../me/arcanis/ffxivbis/models/BiS.scala | 3 +- .../me/arcanis/ffxivbis/models/Job.scala | 3 +- .../me/arcanis/ffxivbis/models/Loot.scala | 2 +- .../me/arcanis/ffxivbis/models/Party.scala | 5 +- .../ffxivbis/models/PartyDescription.scala | 2 +- .../me/arcanis/ffxivbis/models/Piece.scala | 4 +- .../arcanis/ffxivbis/models/PieceType.scala | 8 + .../me/arcanis/ffxivbis/models/Player.scala | 26 +- .../me/arcanis/ffxivbis/models/PlayerId.scala | 3 +- .../models/PlayerIdWithCounters.scala | 5 +- .../me/arcanis/ffxivbis/models/User.scala | 5 +- .../ffxivbis/service/LootSelector.scala | 2 +- .../ffxivbis/service/PartyService.scala | 6 +- .../ffxivbis/service/bis/BisProvider.scala | 6 +- .../service/bis/RequestExecutor.scala | 2 +- .../arcanis/ffxivbis/service/bis/XivApi.scala | 2 +- .../ffxivbis/service/bis/parser/Parser.scala | 8 + .../service/bis/parser/impl/Ariyala.scala | 8 + .../service/bis/parser/impl/Etro.scala | 8 + .../ffxivbis/service/database/Database.scala | 2 +- .../database/impl/DatabaseBiSHandler.scala | 4 +- .../service/database/impl/DatabaseImpl.scala | 4 +- .../database/impl/DatabaseLootHandler.scala | 7 +- .../database/impl/DatabasePartyHandler.scala | 4 +- .../database/impl/DatabaseUserHandler.scala | 4 +- .../arcanis/ffxivbis/storage/BiSProfile.scala | 12 +- .../ffxivbis/storage/DatabaseProfile.scala | 14 +- .../ffxivbis/storage/LootProfile.scala | 11 +- .../arcanis/ffxivbis/storage/Migration.scala | 6 +- .../ffxivbis/storage/PartyProfile.scala | 7 +- .../ffxivbis/storage/PlayersProfile.scala | 11 +- .../ffxivbis/storage/UsersProfile.scala | 15 +- .../me/arcanis/ffxivbis/utils/Implicits.scala | 7 +- .../scala/me/arcanis/ffxivbis/Fixtures.scala | 5 + .../scala/me/arcanis/ffxivbis/Settings.scala | 4 +- .../http/api/v1/BiSEndpointTest.scala | 68 ++-- .../http/api/v1/LootEndpointTest.scala | 29 +- .../http/api/v1/PartyEndpointTest.scala | 14 +- .../http/api/v1/PlayerEndpointTest.scala | 12 +- .../http/api/v1/TypesEndpointTest.scala | 9 +- .../http/api/v1/UserEndpointTest.scala | 28 +- .../ffxivbis/service/LootSelectorTest.scala | 2 +- .../service/bis/BisProviderTest.scala | 2 +- .../arcanis/ffxivbis/utils/Converters.scala | 3 +- 113 files changed, 2561 insertions(+), 1993 deletions(-) create mode 100644 Makefile create mode 100644 src/main/resources/html/bis.html create mode 100644 src/main/resources/html/index.html create mode 100644 src/main/resources/html/loot.html create mode 100644 src/main/resources/html/party.html create mode 100644 src/main/resources/html/redoc.html delete mode 100644 src/main/resources/html/swagger.html create mode 100644 src/main/resources/html/users.html create mode 100644 src/main/resources/static/favicon.ico create mode 100644 src/main/resources/static/load.js delete mode 100644 src/main/resources/static/table_export.js delete mode 100644 src/main/resources/static/table_search.js create mode 100644 src/main/resources/static/utils.js create mode 100644 src/main/scala/me/arcanis/ffxivbis/Configuration.scala create mode 100644 src/main/scala/me/arcanis/ffxivbis/http/AuthorizationProvider.scala create mode 100644 src/main/scala/me/arcanis/ffxivbis/http/HttpLog.scala rename src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/{PartyIdResponse.scala => ErrorModel.scala} (65%) rename src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/{LootResponse.scala => LootModel.scala} (54%) rename src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/{PartyDescriptionResponse.scala => PartyDescriptionModel.scala} (65%) rename src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/{ErrorResponse.scala => PartyIdModel.scala} (62%) rename src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/{PieceActionResponse.scala => PieceActionModel.scala} (71%) rename src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/{PieceResponse.scala => PieceModel.scala} (67%) rename src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/{PlayerActionResponse.scala => PlayerActionModel.scala} (84%) rename src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/{PlayerBiSLinkResponse.scala => PlayerBiSLinkModel.scala} (82%) rename src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/{PlayerIdResponse.scala => PlayerIdModel.scala} (75%) rename src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/{PlayerIdWithCountersResponse.scala => PlayerIdWithCountersModel.scala} (84%) rename src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/{PlayerResponse.scala => PlayerModel.scala} (63%) rename src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/{UserResponse.scala => UserModel.scala} (80%) rename src/main/scala/me/arcanis/ffxivbis/http/{ => helpers}/BiSHelper.scala (82%) rename src/main/scala/me/arcanis/ffxivbis/http/{ => helpers}/BisProviderHelper.scala (67%) rename src/main/scala/me/arcanis/ffxivbis/http/{ => helpers}/LootHelper.scala (83%) rename src/main/scala/me/arcanis/ffxivbis/http/{ => helpers}/PlayerHelper.scala (85%) rename src/main/scala/me/arcanis/ffxivbis/http/{ => helpers}/UserHelper.scala (74%) delete mode 100644 src/main/scala/me/arcanis/ffxivbis/http/view/BasePartyView.scala delete mode 100644 src/main/scala/me/arcanis/ffxivbis/http/view/BiSView.scala delete mode 100644 src/main/scala/me/arcanis/ffxivbis/http/view/ErrorView.scala delete mode 100644 src/main/scala/me/arcanis/ffxivbis/http/view/ExportToCSVView.scala delete mode 100644 src/main/scala/me/arcanis/ffxivbis/http/view/IndexView.scala delete mode 100644 src/main/scala/me/arcanis/ffxivbis/http/view/LootSuggestView.scala delete mode 100644 src/main/scala/me/arcanis/ffxivbis/http/view/LootView.scala delete mode 100644 src/main/scala/me/arcanis/ffxivbis/http/view/PlayerView.scala delete mode 100644 src/main/scala/me/arcanis/ffxivbis/http/view/SearchLineView.scala delete mode 100644 src/main/scala/me/arcanis/ffxivbis/http/view/UserView.scala diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e21274e..85c0f89 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,9 +25,9 @@ jobs: uses: actions/setup-java@v2 with: distribution: temurin - java-version: 8 + java-version: 17 - name: create dist - run: sbt -v dist + run: make dist - name: release uses: softprops/action-gh-release@v1 with: diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index d83ea63..bae636a 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -17,6 +17,6 @@ jobs: uses: actions/setup-java@v2 with: distribution: temurin - java-version: 8 + java-version: 17 - name: run tests - run: sbt -v +test \ No newline at end of file + run: make tests \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f2eee08 --- /dev/null +++ b/Makefile @@ -0,0 +1,35 @@ +.PHONY: check clean compile dist push tests version +.DEFAULT_GOAL := compile + +PROJECT := ffxivbis + +check: + sbt scalafmtCheck + +clean: + sbt clean + +compile: clean + sbt compile + +format: + sbt scalafmt + +dist: tests version + sbt dist + +push: dist + git add version.sbt + git commit -m "Release $(VERSION)" + git tag "$(VERSION)" + git push + git push --tags + +tests: compile check + sbt test + +version: +ifndef VERSION + $(error VERSION is required, but not set) +endif + sed -i '/version := "[0-9.]*/s/[^"][^)]*/version := "$(VERSION)"/' version.sbt diff --git a/libraries.sbt b/libraries.sbt index 7f38267..cc09bee 100644 --- a/libraries.sbt +++ b/libraries.sbt @@ -14,7 +14,6 @@ libraryDependencies += "com.github.swagger-akka-http" %% "swagger-akka-http" % " libraryDependencies += "jakarta.platform" % "jakarta.jakartaee-web-api" % "9.1.0" libraryDependencies += "io.spray" %% "spray-json" % "1.3.6" -libraryDependencies += "com.lihaoyi" %% "scalatags" % "0.9.2" libraryDependencies += "com.typesafe.slick" %% "slick" % SlickVersion libraryDependencies += "com.typesafe.slick" %% "slick-hikaricp" % SlickVersion @@ -23,6 +22,8 @@ libraryDependencies += "org.xerial" % "sqlite-jdbc" % "3.36.0.3" libraryDependencies += "org.postgresql" % "postgresql" % "42.3.1" libraryDependencies += "org.mindrot" % "jbcrypt" % "0.4" +libraryDependencies += "com.google.guava" % "guava" % "31.0.1-jre" + // testing libraryDependencies += "org.scalactic" %% "scalactic" % ScalaTestVersion % "test" diff --git a/src/main/resources/html/bis.html b/src/main/resources/html/bis.html new file mode 100644 index 0000000..28032d5 --- /dev/null +++ b/src/main/resources/html/bis.html @@ -0,0 +1,349 @@ + + + + + Best in slot + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+

Best in slot

+
+ +
+
+ + + +
+ + + + + + + + + + + +
nickjobpiecepiece type
+
+ + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/html/index.html b/src/main/resources/html/index.html new file mode 100644 index 0000000..c524f14 --- /dev/null +++ b/src/main/resources/html/index.html @@ -0,0 +1,183 @@ + + + + + FFXIV loot helper + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+
+
+ + + +
+
+ +
+ +
+
+
+
+ +
+
+
+ +
+ +
+ + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/html/loot.html b/src/main/resources/html/loot.html new file mode 100644 index 0000000..8996a2b --- /dev/null +++ b/src/main/resources/html/loot.html @@ -0,0 +1,338 @@ + + + + + Loot table + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+

Looted items

+
+ +
+
+ + + +
+ + + + + + + + + + + + + +
nickjobpiecepiece typeis free lootdate
+
+ + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/html/party.html b/src/main/resources/html/party.html new file mode 100644 index 0000000..b151f36 --- /dev/null +++ b/src/main/resources/html/party.html @@ -0,0 +1,258 @@ + + + + + FFXIV loot helper + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+
+ + + +
+ + + + + + + + + + + + + +
nickjobbest in slot linktotal bis pieces lootedtotal pieces lootedpriority
+
+ + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/html/redoc.html b/src/main/resources/html/redoc.html new file mode 100644 index 0000000..6b67c4d --- /dev/null +++ b/src/main/resources/html/redoc.html @@ -0,0 +1,19 @@ + + + + + FFXIV loot helper API + + + + + + + + + + + + + + diff --git a/src/main/resources/html/swagger.html b/src/main/resources/html/swagger.html deleted file mode 100644 index cdee79d..0000000 --- a/src/main/resources/html/swagger.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - FFXIV loot tracker API - - - - - - - - - - - - - diff --git a/src/main/resources/html/users.html b/src/main/resources/html/users.html new file mode 100644 index 0000000..3e28c9f --- /dev/null +++ b/src/main/resources/html/users.html @@ -0,0 +1,230 @@ + + + + + User management + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+

Users

+
+ +
+
+ + + +
+ + + + + + + + + +
usernamepermission
+
+ + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf index fc8f9f1..7701121 100644 --- a/src/main/resources/reference.conf +++ b/src/main/resources/reference.conf @@ -1,62 +1,71 @@ me.arcanis.ffxivbis { - bis-provider { - include "item_data.json" + bis-provider { + include "item_data.json" - # xivapi base url, string, required - xivapi-url = "https://xivapi.com" - # xivapi developer key, string, optional - #xivapi-key = "abcdef" - } - - database { - # database section. Section must be declared inside - # for more detailed section descriptions refer to slick documentation - mode = "sqlite" - - sqlite { - profile = "slick.jdbc.SQLiteProfile$" - db { - url = "jdbc:sqlite:ffxivbis.db" - user = "user" - password = "password" - } - numThreads = 10 + # xivapi base url, string, required + xivapi-url = "https://xivapi.com" + # xivapi developer key, string, optional + #xivapi-key = "abcdef" } - postgresql { - profile = "slick.jdbc.PostgresProfile$" - db { - url = "jdbc:postgresql://localhost/ffxivbis" - user = "ffxivbis" - password = "ffxivbis" + database { + # database section. Section must be declared inside + # for more detailed section descriptions refer to slick documentation + mode = "sqlite" - connectionPool = disabled - keepAliveConnection = yes - } - numThreads = 10 + sqlite { + profile = "slick.jdbc.SQLiteProfile$" + db { + url = "jdbc:sqlite:ffxivbis.db" + #user = "user" + #password = "password" + } + numThreads = 10 + } + + postgresql { + profile = "slick.jdbc.PostgresProfile$" + db { + url = "jdbc:postgresql://localhost/ffxivbis" + #user = "ffxivbis" + #password = "ffxivbis" + + connectionPool = disabled + keepAliveConnection = yes + } + numThreads = 10 + } } - } - settings { - # counters of Player class which will be called to sort players for loot priority - # list of strings, required - priority = [ - "isRequired", "lootCountBiS", "priority", "lootCount", "lootCountTotal" - ] - # general request timeout, duratin, required - request-timeout = 10s - # party in-memory storage lifetime - cache-timeout = 1m - } + settings { + # counters of Player class which will be called to sort players for loot priority + # list of strings, required + priority = [ + "isRequired", "lootCountBiS", "priority", "lootCount", "lootCountTotal" + ] + # general request timeout, duratin, required + request-timeout = 10s + # party in-memory storage lifetime + cache-timeout = 1m + } - web { - # address to bind, string, required - host = "127.0.0.1" - # port to bind, int, required - port = 8000 - # hostname to use in docs, if not set host:port will be used - #hostname = "127.0.0.1:8000" + web { + # address to bind, string, required + host = "127.0.0.1" + # port to bind, int, required + port = 8000 + # hostname to use in docs, if not set host:port will be used + #hostname = "127.0.0.1:8000" + # enable head requests for GET requests + enable-head-requests = yes + + authorization-cache { + # maximum amount of cached logins + cache-size = 1024 + # ttl of cached logins + cache-timeout = 1m + } } default-dispatcher { diff --git a/src/main/resources/static/favicon.ico b/src/main/resources/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..78d37d607671885d6bfc6274838fd30ad0100de9 GIT binary patch literal 30506 zcmZ5{byQT}`}G}iK)OM?yQQTOK|-WaN?MSV?h>TCOFo3sB_Sz@(jbkrbaxN%JABvs z*E=qkqm1|7Ip>M}?7a^J!N9MF7Yw3>h}h2gc|ig6j?60Vo5PM9|z1#enzMZk1lRiHaa&kU4 zYbvItJLP?Lj7$(0fWk-%!+0)@^#8rh8keSMe)s`lu(eZ=kel4&V++E#0Au8FG@6Vj zbkFg?f9!}tM?rh_dKqnPXgT)Di-&(hWCY4j$D;kePaq5e_3Tb=5Kio6_-lTNA5mb> zJ1Vhv@r;3zQLRCM4_OshE+4;VEbkDS!3g;@qF&q?LgP z4s9WN{b?uaKiYSC(DD|p6vFZRYjKXj6RV*t_GPcTJ81z~pXz0d=N-Lhe+EVoZ!@ki z=<<>#%k--M6_u5h;eIe&ocvEQ)(CpTD}rRG7bzBGNr%Sj+OB{^FtQ|_uNTl$+s4jG z@tU>J{CKrjuC}H|5`tu(yY;7VjUjD(nhWyq_nR+}vE4-jtHlR>I%?dd41FE>lQw+X zea=a-R8viK&CHFjaNK=&x*8M8>2SU4xDx9uM@4b=4`fEiK#B`khyG0OFyNzeH05rf zOuMZi<4PR1<+C<*wq`+;kWaz}uSTA}Oc2DF$0o=4vte|zf1iOHYxzl<0#xI5wtep< zk@FcAEd}Kj6Jtmkz$$Ivweq^+$Tws=r^ zeRp$kpOTZ~M;wcQUTSK(AfdrbMC3w4NzpT~@gH__inR3r`DV42_iyJFv5j@cC{O|7!`GI#8&GY`s56Z z|Aq(r;yMkZ@$;yW3&Uv$?-|aks*3vrVI8@Ka3IZ$uHxrDd_7vDm!t~W^SAQyq!U;Drg%phdCB~y z0zqgv)tm9ec0ujMi)MDmnBjv9QW_D*r7@NqkAElHr8?P56?9P6%CG3hPLujp^E-|r z2G7Q*mA@{udSAGh(mE42Xm!!TC7~z0jYtauRlVM*%-~CGBgG5uyHR`nnagB>WW=A+ z$rmQE>l(Uhp9>62oH!d8{O#uNrlLc6*us*Ml4DoHi{>{KlS)cTo*{Uf->DS{u~<1d z=fHz>+?ddMS(up0(-Od}YkF`Q<-vYZ@8)kqO?E@x%Y0R`cT*Z{}Qv zeYYG%{`O6~E3>f{I*MhNXRBG+9(j!OD8cI;7VikL&W*>fg>NtfLDIc^94=HK#LB@D zAdnB4w(m_RNETzoaDMo9)ycSlz&y4mv)>R1B@)qt20C zi?nASpWjY#qRDVAOyOJw*S43GWPlXAJNiwdXk~8x#>`c-a;o`Yy4viKZp9l>htUEh zaZF6imMKGb!(x{q(PHCbwd^5vHMRXB8v_H*8t-i5B)p`gBudGhvf4XLxh-9^Jc%kT zlo;)b`bsZrnU+P(NJXZm?LyLRiT4J*I;BA4|6 zPewMjuljWDMK*fXZ@VMFwcMHMU7$_Ulj14CrVHX}o`*wjc;%6$#JssK9h{gw z^*jF!o;mq3)-R%VxPk?`*dG5nrdsVW`$UK}A}IQ?@SXEYw6GtnLy_&QQ`%=?EH-z~ zwI|tc5R(8!8b_yRczD+>B*6XMpQb0xLOJpsYQm!^vT?^T`Js8Ym@G=lwq=nT91EL=X5KXS1ce<=Q`oR9l6l|IQBb1Ujcd zo%I@sj}uc`k7V=Z8Gp%tu?<&|w=;H)0pO-MZZX_4GrAf?ERgH@?u;=1DmtV(hIY4O ze1FX)Tf`?B*BwI`uE4=##Ny0SlTR8?GWhm&(XK!`0%A4$6?N4fLnUrBIZeZ301v^` zl{_2Ql%fsU(%p}?`k>zcUSw_;_iONE&5?NR5{4J z_#dUj14Uzf-iroWn;j5GuJBj!zt+c>Ilse!xD_KJIjN?yFRz{% zx@^zthI$}hfCqht^eIK%tKD8pKMl7H3`kR6wy}Kzi&^Y8{7rz+hs}{mb*bCar`ckz z-)q3$L3XqC?}tM$2>m_=u1>(|q*hQou5#(xaV@)zYh~TY8Q}rnS~!{GKhSnu?|iS2 zxLQqgKY`u*rP1c>Bo_uJ>=@dBFwG3hGGPy6m)=fdkTGA(d z(_a5%m2fpIvJzDQ%KzoS*v+XYZ|@4ZY}7Lt(VomO^+U>R(2vkcIOlQI3PgaC zmmWN^%QJ*f8QUA`?Nv$_by@$-#>Q4WJFllfG>|QRQP_OY=FJ`z$S}m&^5QG*^4WU% zU)xkq#huCW*L~w(HAzcK{!;MC(rOeLX=&AcDJ!#U|Gm;{KU1^TT~d-zMD&$(uNey9 z)UY-+U0x1EMwdii5wF5qjOfVNK#|{|;X}f+Uhlz0cqs%tua%PHU04kjzCXz=)zs8f zpJKz-oFzmNQIq|=SVL_{HYOdK4xdZ4E}rHxkYHm$qk>}jZl%7l1^()J7oJK=^j zKeknNf8OYiMZbt4234GcipR+E6%&WtU9tluJ4; zN#FV3ovhoDJs)HcPrM;dU6;Eob0A)uvWus=JR^YcIR!@Omv;G0Is$a8&UP+PKi}e(&Vh1H>&?< zirM5CyMl-Fr)q3fpjDsC)NjL4!sPd@U%Q&CArBWvX&JzGg#`tT^Y#sz+S&kdz7@-) z82&6n1%)ndOtl(S9&`jED?W#}O;vg%;)* znXj5DWc2_V7AYk>9RI{KC<@VtI-9#YIUUhePQCU*Tps0LEc*LfE$O#)WTa!>N~a~J zkcD>X6Hv)pVh2!wfl0fDDao;-p56wX=sAXe?T-1&axckI3AP?l&I9zh~2WC5^Ode=v6 z+o$F|arRHW&+UXwJ42XYx2OzRDs}(5JU!8AJ|gnFVmbe*6AivizfvM<|HB$P^$20` z0dni%d3Q_xul8qB&T>~+gEwf6?HWYg_`}bVQ&We(U*F#yoX>loYti|P9Qh&FK$Jyk zACO0i)UvN%J`{Ba6@5buqO#Ww_B+_ZbG(B)XA$f2cTeF^eo2Y*qVL67%h}}X;S&!D zd+6}y_Kqdj#~F-_i5I99_zH}Edj|mg^yNJD?kg=Rk=M_r6JUU%x=Ho@b8bmNbL~|u zEAubt;3OYmjqS$v!$zT{SlzI?%Nxag=Jk$KuWD&OC*n>C=>M72^YmB zpALHE+rb$YAR2?_it_7#F;6=0qv{{tMO1s)@Su_s_Jhqo|5-&*KlSp>^VPAh$q&oN z3BV!eFHpVMpBEtE)N^$F9Ye(ulx=0?fz%Om^t>-F_uz#QR+aNb`T9`pKM1m2B~@+E zG25KgoW#z{A5+ zO2+s0XYrr&@^+zZEPCQ&N=acSv^g-`_vR(qAC8e)QOemNYX zWkI7q7Wk9L>=-qLJ`TTW=^c)pUh^%*2-E%7=Pb{!5$TR@bwq)J1?RyPaJlv+}1ei&<>s zaFLY)NBS3P4?N^Kk|#4XW$b@j);NxflffW1kSXjSC7Tn|l5M#D zf+ypTIb6;(dQ~uDvHT~@h;!)^lK&j$Rr$aNMB62|k2JZ|dC-Q`1YkK{2MK08|#;U#~09Z*JSL9}96J zR%q@oxubCRgDJN>gZ_eM3wfWxsfU*4damP;K2f#cRI$Byu40A2N2w~}O zRT1_z4y4iNG~qdThr{6d+a$wCyYa?CrLRLRPktk4{3X`wV901{T117~CT~3O2=2*x zvSR+Jp{SuD(VE>_^i(#@+kbmt7n(M5#KAPdcre>gX?eQ&=eG%#9&uoLyX{oPy&}Yv zgCb3bQW-;e^}wW*wv2rcOH0KQdbTTO=ED)?sDYHQhoR`9(Gn!y6}q z=}&L!>-YY)O2eIMBf1I;3+?!i9}N?nA!9J*VfRQTD7jUXlym~@jj5$QXu={17JkH) z%CEg$R)Z)tpOw8(u;OVsD<-w~ZZ$5Lv%hnVkhDeX>{-1D5JbOs@V}Ym=6YQ(F+V~# z0kaN4PKcomS=!)|qLFR%eVT&;!D%k}1wJ>@l#|hkTb@i7IWQA)bbnJS8*??h1r*wW z!)*N?p~R1(q!cPq=Mc0uuE3d{c)hX}G!{Y_RJLYG9B>`$k6=*Op1@j0h9E6%)qfW& zleDr`c2~!)n#F4Kk$A_iu_ZV}U!6i)_a9CMb*qGg(NF~MB||obNzWjD^4W;XBjh>E zCb}84)t__*NU&w2x6RY_J=ziZg=vcMB9u7gMMckyzkk@81!1lmzS0lasEv({8~jd% z>i0EmpB@XkBbivdPX2x=d5<_ztZ2XUBFpiBnNeZPci1sTn(PYH=!ZS=nQEYJ9%L*=@aMl-GI;(Hfb+qou_mzv5!mf~XvB$bLx#m7@sccWT^ zv5uKy2X+hu5P8kfo0vi|Q*|jsTiM~GNS9*HS z-3}J^q}+DN%D;Z4dy&fRE&h~Yg+BdjeM^haNuA9YyQv$>iUQ#}S8oH;XOudKJo1?U=OWodYWX(m_VTqmRzu?~(^paRBYt3K`ArR#o_$2<)V31_octFnc`ZD#XYMbM z^U1QODz&~ztd8*evnf||bIUJq_l9zr#`1agSn5F0<9MpXCoH3OF$QI()x-Xe?lQys zR!#mTks5oLVrV3tc3d1@-QFc^Z*K=TyC38%f=H`1sOxF$Q7My)_1P*+HIji}dD7rf zEhh%5(CM*=kdFqvPPZ^WzZ-u)TOUojGcv{UQkl=^^3Vm)waGY5{J)i#S-vxg!x|U7 zcFOxl$Hr_;bE0-$fA(&mCg&9&33qcnYW(7vDtgdUcg&w1@MR=XVPE0D)yR6n^0?e6 zD)Dn!?Xkc3vag60l(dNoL%WtYlM)h~hirameYzEf&#!o* zU|z<_=oFe52=P|c{62moJ-SobE(x-$n$jb|=7*nKRL=V7lD}awnUo(Q829PrOB%9c zzUD|n9NFs2k1N9MSE~C}rd|7CHYW0aXfr=k5AK9h23BZhdA&PugEv&q(&b$?r9d-DHE-tj;AMtgMv+?*kZd(DTO%URN(58Zgfl0GT=g0CO=8h2Z9IKnCqofMU%SsPa>#i`#`RCYdVY zmu75xJEvh5azOB_jJG$~Km7#JP(Bzc(!QwG6a45Nq3j6p1tjat{KR;voXo-9{;%9d zv$$1XUtdGd+lkgdW7N*6XK-*&)c5){76i}vVePP_w|%D2`vj8#19rL)MK6-=(g%dK z2QcF?jq2E||2*?=F(fMG%oMYsg5u(a-O<7q83mOJaaY266I5D?vDCixV_Pk72u@c% zB!1A6d_v0E_(&R?PQJ`VFSo2_;HzR(_aUXEv$=zc8SBWJSYaI~5vK%1H{}a!Er|g0 zcTHAzU;Ru}=WIG1eUbaX8dtJnPhqQ25A^pxb~~HUy>|gT%h&MDboru*)-kY@fQU#}H5v#nB7R-ioXua@p#qn#}5Vd?ZNHaMP_}*Y-so1|$CJ?rpv7&&zeDXUIDI zQtDr4qP8(Odjpx6sv0c^(vBJcf#YWOhBR@(Q}eU+-^6390ToNhC%cqKvO=sa3mGM5 zp^Cah)EskrBa}IF_3zP(3Jcvz3`O`a@~W(!{V+5zB-F#l+x^>WJMmERNlo-1JvYMz z{O4HJsP4Kp*j+YbMe|9isTUFsHcTN)zn_%)`}gPG)zOaN8U5(tz|)trSYgzOYce_M zeLDwpkgNfpPd4@a`BYm&i-?qzN(`j;{vP<^f9rj3a@P7&5B|1zp5UiZeM!4VaUrz& z6IH|7-G~>E_ydd8>-i`CugR8aRAcsSSL;p>v}WJdhwq(ws~uDdW7HS1P@h>EH@J+vPc?K4z#|gPZ=^_Y9vwqiX5<5wpKi+ z3I`3p@9t*t-j5eVX*LKjF1c`#ngX_bB<*J-yIINGC@hI^NRJSv|0^vIt+}eI%5JqM z-q_lJFh4BXW-XH7gW>Pod&Gv~!-N}#M=y?VWRu4Br2EO3Ya>2yXmUylzwR_Gkc{l` z_o>SMgggGB&tUJ#73 z@}&`7{8C|!vM!{sY4Py4(2e!mI|*T|b14iud|EEYfm)LvLG2dR1($p<9AEO>zORITHD6h-<=r2v46_fvT?!8Q5rHOp;c%a_`5ahDw!EJe-Eu7{9} zxyeE|!q@JviP($3=ZE!a@>HMN(vLSDy(iACv@z&od+KfE*~eD=rkC54)vLHSQ@f!F0>QuiVHO;Qzxkq?Ep~R~z2CH< znc6=wnKdH#!C0Biz_Ni-%=36v(In?na8J+s+9#5L-h2$S9U+%rQRKq76oOw+qta7I zQGKD7nVFg1SgAXYh6gPJNJsD3dGbX zf1_<>tJ`}oFE1X|vAI9aulHsf+C4U_OpQ6;ROLaI1t#IY4#r5h`27zYMXud9M?T-9 z)A$^wEFSQjd$o%DN-ko^hW~RE6Bl`+h@Ou}^7)hN&SdiG!PLEq6avwD$?p_J#wsc* zn%yn5Ki}*=2JUulwnX9AQVmy<^o&T0Pjf1&&q+A?m>7s$Rg?br6YOnJ4^RkZ$_)wy zNRrT6>!GG%@KTn(Znz3uz7|9uTxy+`pwmW z;ORd9fzp@@^2R2SetNo9NyN60rElhqb7a?YPs?WKBlR$H{PGlYRm~GttP1%lSJsGx zW#{H$WhJF6RcGhpmtkG;iZao2xZiQIw_j>8`R-r1EFvxG5OpJ~EhzQ~ccDtFs;eKE z+@%BJPr>`2Y$8uW;B}$4i^KgQa$vBSxLpwhUAkltF&vxz`y{$0{E;S7$ghV$no0#Do_Dfi3>R0Hv%VOg&#mY(E$`U&m#_Yc6LAw|820J|E_Cb zV4xfOJ5d#0eL74VLMtWZe+!<|??H+5zdjvPpJFh-*hnlsrW>yr`1y@ZkwgGN0f%0n zpagiL@yB*(K{RfW{w-e6mF^p-ZkIqI zex#1fo6CZ)b}$O~RbZknN)_@S!Z=rnbWz*&53)okOkSB^q^EmInS~M!7SU^N^fr-A zE>y?_V)NM@zpx92p@nr}6px{Xb-}{M^s;c!DRIyRRfCI4UJ^6u{nvr;;dmNuc>15O zzdm)U0=M$bC@qqGD#y)3pM=xB%^}jOlL>mG7 zh2qs~6FwBb_FQTSk1EH5h1TCdO>_0Rzq_?Se}u^%5gAd^q~}#oP+$#)%nw#pR^ET> zY!VXyZ#!Q9ewGAid-`9$e&wbusI{I8VTk{npZ~8XlBeww;lAkGFtz7adeC~Y;QAnP zyGG;-V)Uovv+99>`_hhB1!=x=zX(*NDwEs;^;hdxxu^4-rF#b=>=`~5`*G_nY z>r3S)A#uh;$gg0G#%?o(#S-_3M-A2)BvZ~y-}Xgh^Geu3-v;Dv1TK}BR*ThePUqRq z6pcZvN18DkF5M&lWU;bu2QKIc-X=(Y+&CM;Nqy!M2 zUxvj#Ri9&M5M{Blu{~E)v+uett`5Xs%g@i>18QO`@^X&9PrZ4yWsu62Ze>y<9US^p zMT0HhkE++5iSFBe&~~%m0*1CPUy6!8P9dj-OM?vmb-FcnFxb;$jo-p0(=n0afQ^xY zi7PgXPlEkS6bs8xJ|HjRhljukIkV_1jXWC)OdR4Qm27_3`JbN%-2t4N0*GV$(nnHq z^I4Y6F6DC%?oZ_w=?>vzTOT|ms3+E(ZAcA2Xf!XA8qeL1kE!*K2}WP!p)Tq->+t5k;J1&_EJ^SY8jZFvSp`x@JK@6^0x!43=A&ov%_CCRFH*{A@_dkJQU-K|~y zMtrW-y~195PZJC~1R;Hk4~eg)CM(xRj1ai6^@~8{FkU)I==Z$);`|mMZF4JFl`%8X ziM17!oc`>iJ|Y?V+v@Fp1#ry}nTX>Es-I!yw=9Bw*9TjT8h|W5KI;SNj~D%J_FUnE zSOYS>xV7baNmLfqt8T$cBu{%YRrW9q4=Iz$BqJq#uj|@&Dw>DH){->EK#CM)lo5Ta zk@<)Vm*jgfZmFeFpAafl0~NqO$N9A{-mmEz50YJNmqsG9gd6!`U2&K`^V9!U@GSc_ z@R|mQO-|uz&-EOZQ@^dWV@=85;^UVS_p)qEWP_5HorH% z?9vC3!+tQ~E&^(S$#$wt^p%d05r)fQxZaqNO2c~v6c55;cnh3%N4yOeLl%QWp}?J1 zqF(aSyHuzA)%C@};&qJpKG}HTi&P=W+=GH8OhMHyAnhNu1HIBtF?^6T-ncBW=e#`0 zAKUUIRmPV9;<%C!+3~qt4xiy-|>b3 z%ge#8(?+&?j4o04d>j==P?61a1JC{D=AQ#DAfj5f0b5Uy=(Cn*H6&Du^5_KdP|vCo zwRs33oD{iC9@3~Ih+ov|#QKaS8S^7}_`OR%SHrm!$nNH=Tfa>hm-npzbMk`5$2SWiJ7ZOH1k-uEYe5LYMYfW|*w z14Xg|>u2pB%Mo?ImmdyQu{U|0|20B@0bf~Voil}$ zA&n+DmBX`(!0HJK)IPj%gE%?rH}b#zMq}#Zqp#XZ(X;aNDuAg}S-hG((L4o8X9b?I z6bmIrBpG={R<{KVvqN#12ykwFcj!$hME{6GK6il5 z^+28+?%aU4`5h8wssyj^)9sojiBf5HaXx1QLG(rAKTD5RqlI+EtgWli`xqS~fBs0_ z;9AAAZrHTv>Q_jxc|UFQ2Or&+uU~r?0nFhLo^$MCBnu96C80SP6#pK1 zc25aW0%fb;_o$mxk%ftA?Fq>&u&?p@-yxzMz)0N(&@heMb9WqGSSo#~hJg z0NT8oW(4p6Ol*GVHsa~KMnEoxK1bbq_h*&zx%8LD~uZ| z9D%EJ2aKJZnELL}#}im`lb__Tn^zNF{B1exeB^>`k<0mG$W+v{5KpIj$&}>)5e}V?4E`9Z$z*> zw4}s9ZsyPeG{57M6I`kDX|plEtF_cFUNqP+$+72BzRcZoW=Tw2B&fH3{vyFwoce0U zPmnseCFo_*BfR)UYyrBL2aPK+0uOew$Sk$uzeGXaz%B_KEcbT}lNE*>1OfbdvsdZQ znY^s};-tXxIlRo(fiRnH7YpjfT<5F(7w?`@iG2w7(EPX>k>3csq1@usl&nX!2>Hdp zjE^AL-nTtaCc&OXsa)0Op~=bL>oc!Z4T;x;Ys9$;V1LYeUb~XPzQ}~b#~p7tF-nsw zkr{mUAMD!mNo<;K4~Z-cT$zP>^+w0#E6Yw~ljI@5`mrFOX<6~@WXKL!SOqDYIWv%g z-vrJAN8#S2D{nyFEUWP-g&h}j;c859`$wi(ECf-?CTyJO#9jh7&^E+8ed(f$&>N|v zKq-;r!Re`qL8rp@a~Q8MP{Eu^fjof}*PNL#U4qSw%Yv=2Tt4i%ABx{{&h8Z=AT?dU zQ$atps_J6nMt=9d2%ygq8M+KUp*fhVW-!*&9+DfA`~mM`8WYK1He=m=jW(>H_X;q{ zhI@a%y+}Bf6I52$|B=8P4E9+cX1?E)|8)qXV*<3U)5rFW%Q)KFPow9_t2r6iZf z)TQPjmsQf&@{FlAk=^*!n;sifC% z@$d{!0QP(C7r_A$!_dVgu+9n7zlk+Voo7kVo7@D;)zZnSA~W5pctK*{(5ztQEI*BBgbk$2TW5$Em@_N@M&F!cdXZ9Q+e5Hlq?Sj7jE>XtQd^tY8Of$N?VIzt4FR~l zBzoSQ%f(aD4}JZQpS#Ww{|I*F@i|ucM0*E-G22~7hh%TQR97*jHtqtCF)Om@GkV_V0uIh)WQYnYc&DPy#McX&dI8`-#)j z)1N+WW}t7Qqyt{4FFrt_XukwN7DtwQrBBaKW5t1KOff?SuiJ0VNVsoBVpPD{rnrho z6m!Pl`tC&vr|5L4Zsj;oKI*;}yVs*AvjNe^Wo&HhoZGm~ho_UcK_{fFSI*Z9$sj%I zGMsO%q_l!!8enpx=z0A3Jb-eybG-M-o55^(3#Stq{wts+U3SiH8CZ=8|75vtJs_h% zPg(n}lvvoz;&0NHFdX{dBG}tkjDq{(V85ql42c6oK+ah|Py4`PgM%^Os?OumgK5TA z%D`(D=xv=*+J@d#h2a}8=(TlhoM;0#S|8v^-?c9u{Pz(}C%Rj%uB|KpZ2yyCFs-C= z=vENL8?PdHEINv(EWVp0H2!Vh&KEenJNt=c+)|vPT2a5zarp-@hSm4=_S(#h@RYMg ztN=Ex#REj13xJu%n6@t>E@MnQP{n|TK!K*c!;V+Fu+5?voosb_djWy_O6uMRft@YB@y)@yQbso5_&DO}149wL zwzgIXXSuB5HxBqV{Omtjxt~bkd=LVCz4o{!b|6L|mQN8N`fX6x9bG}MaQV+x8L zlG=f`Q!IQLNs*E#2S()E@`|0e5&_w7<75)A+Kz}|0ge|MPSwgHKUhw%`srTk>wonJ zZTK}ffw8#@B&+Oj>AJVF9bFE4U{_nnoL8-8`BxiiuoWq=R4q-$@#$Twv;6}b9Y&E+ zCVy_6`~i7NN#S?YZ6#P18*s#_t*u${eY>P07g${>6*O22>-5=?b`l|IxtF=jr%&N0 z>473-4ea-#UPp(K;w&tMjMXS7At3{@A16MN6?Ft$#f-#x#*Vx+&okx5Os9gO5J*E2 zNW1;RQjR<24Ss0IU-txWeI|5E%Xr)ZX?1P}sm!a&)kajT^!S`Nqm^h7+)&uN5@pMq#h9-QBkH!^~E-HtAVs? zjAasRy%xp9ple!4$F_8n|IWSih80TM-hOvqWhy5Q6lCU7$>wimE&00-E}govGL`Y; z{$0eMs$1zsPI%;PBF6I&U;JG-ia#!r9A9OHe)IY!;M|>SU)S3?7Sa|H0dl5&23$w? z=Hj3-#MKw=oVzBAJR0+C&xlC?^~i4=%jvQpB7c_ifaYLI5dEuqiu#zK&*cwv0YHYu zbcs2I4GgdpdBbA13tv_WDc#84b%i8@%RedGLEws~r~GW`yTI zjCsS=i%FEory1cga!Id9p0-S*pQ2Abfh&6PeF|>?=7Mr+kEKAVXDg3hKE~42CeHOe zpP2)`zf|d*ljz(l1s8RstN+mPd&z@#c-x*jOsCQ{4@7-|jYuL6yRHCX{|r|_V>^@> zp7i#DO2pCR2C%b2Dp?{&H-IIv1B2Y>9WUa6vUa;svje-UaMl|h`obivzQGGD+Tu^os`?col@_#1?uKSmVmv)GEA z!nl5f1ZkHGzm^CONmb34aZHL=7(J8uY8~tw**JOYNt$X(zTkWU4LDU~Tjz4j2oE%cIDb;_vv27dj0BGvr2< z5{&ZLC2iObOCs*E)aMr>&Zbe#QBkJ_BCQR{o^K>rD1u(G(2wBhle~=&MZO~ok+>Sy z45slt{o_7w@2=yUaqA;#JotjAwGKL_aB!=Y8t$DItz+zpXtwezb4GhkrnF?PCI<3V*W zH3-$*9zmA-Ek_Ue)4PYf5}{Ok@LSA+s-n}_g?Z%pxSh40F|K_dB$8z>8S9=)`qYh55Z6r;)p5Q8-$Kpb)b ziD~u#^keay$v1XT#)(`zYl!jST|frWi2q=~k|1KWStKt>J^5-*`H*aa4#t@B3j-_9%FO)?h`E>yvZ zBCbyKn6kSZ){98xc9fsEZ=D{mKSk^6D;rD~uySz)uU0|+JiSS+K{>b*E@ezXdTQ^P zsUJ2t$>s7uL7wz;x1V;5GQEgg@VeQPAS+U9?hw5r*N_9;lz41MRcdikN#K)w!{=BV z$C3H0(=9e**T6YU88lZcm69&Sck`R(%iv#SnvU1#B`~c@gKyi?%hjD-H55oGFE}^Q zQa*Qmw4@Vz-%160Nvxm&1k4;UFsY~Ggq_4{73bwCWuF{#OE*$WGmHJi!qz8~f0M~- zY++IO%D~_|T}tL8$qJkf96r1k<*MQsqU0wv=7{z;%uEQ_lqOMs+l_ko;_Tw2&A8~- zKQgkF)Pg)#ZuavZ3-VS9SSmv*-z)w{eu$Qo;4D$R`eywnA2n`c2wIh=efM@GP7G=u zF#%RROyC;n6$jAe>Wpd}%(gM_AN2cYtFH`on7#*qEL=3Pkc@do31?q^7E9gQxpQ{C z{a;|n!wEAx;5nSfPcux}lbyAouOeZMc|8xxXZz7GfVo)$&Z|AGyo8T9N~H%z^g?AW zW8iLcy2ifW4*EDMTl~SSlc>-Ca^VzxKiqT7CrWrHMA;|Q&$!8XLM&u9$pYA3XKj%v zaLg@bNS?BiF!MQ~mg?R&4isRmDhC}c1hweeSg$%2b>uz$JALBSYJm%^ds3!)^!bAh zbS_pptcBa*_zYrAZ-!)kM8d0lx{vQWtL%8G0+P|+mgh+p8Z9%bB_Q%De<)@;N4_?rvJdDI8rrxg%@jTczCxw#{lo;Dh$0yQEP z^b$9kf!cOXtM-ZAT2m;V${s;y(Aqj$0XJHWH>1WG`{bWSnrVE@>j(*50M^6J)9|ecMib#t2b|a7>%%j z!eIOWE8rfL^g{c*11M^2XJBD=VY1c$?@+N7pxroQm4XicpA^s& zAWKXD!RkVn@#R!yJxDT-o4tnA4o-nd_;3>2EkVy>oiwFXjR!38?IF;z{>q0F=$EN%|GDeJtVArO0qLdeF zX1mc)QBj*g`zY|j;qC&37C0fJp=NS<(O(PfJmN@cTk6eI}37X_t@P)SeFdp;nYZW#ON5BG(f zG$D1`bKqYGbw$1U4i4!Chqqlu4p>MGe0t#8(fj&2>d|tGHGw_-CL-VCLQKXCa1rzn zPvVJy|C4}{hcjpM0{r}=9}I;pPD}*@njCA)aS?pVZ12DeSYq6e6IGBFT(|5xj!p`qc9j6Bo%IER7(RbJ~mc?6_IH#kPoHc6yM*RVN5%?c8fLf9W7EY z+u@bZ0yu+0^5d;zhu^t6t=|}QWY#~>`}X$J(knp9YJ)RENni+m@2$HmJowr4cg&u+ z)nG>J=0i)bzeDu%>2bs>lm=X)lnUKo(EaVgm6BNbflf0A4wfwL1_a{jhzhK9;tOgS zIPx*D54U}ir&S3=?zjg!yt*M6kG0==@%G&|_%tpzYRI#d2T)#h$Hu4KW(pmeecJ1P z6RNttM#!S3m6DuHTwG8P0+&YV0=+#`$ls_p(dftG_8Ln$EkPOT#4G{~vsb7+{3a*g zQH<)U9w{~a)B2CPA$sir$4YkerR<9B5(prQn3UALNax^a8E4iPm3lcJj)3m;6tSa^qjwL2aWuBk5UM9v=p@NXP%!Ck;^xE=~x9NCjAM*-}3F z@uQ{KYLO)n71W_;yC!GV1S1mCA00=P%z=F>3<^pAbcJUsWDkdm$F}Sj+ggz=S2rR% z|I)P^&VDKL<&A9hlP7^~|Ew0OBbL^JM_@OsA%Q2M7W=>|1$b>2&U~Vt_wK+~C;-mQn3i#B)@f5>XnK_La-hP@ z_*4F`tFsEIs_VM&p;I_C@=AlG5+dCp4FZx%NC={oG!lnWO1fJRMOqM%4naU#lH4L&KC~d*4LPkPuwUuk;mPzaEdJ z3RsQ6bS9bGELrrX$(hXTy?fZSZ3k9yM|&yjLe?WiuzPGG+M_G0NRBbq;9^WND*E31 zuG)^_JYxzM+U(C|UZZI1p@>YyouMDTm>M(L1vUk*-`r8+YZ+Yl=TqEh=7iJxiEF6q z(&LGBad^L)=VRNml*WMG#hOp=QcBOkM3+|+Q{FnLI>cVJXx zVqdB_!UPgGq6^oq;RY&S(shkrsw}+If2iqCto0$Gjq{#s+wu#gJ8~U1KkW&zTOxGT zlo0>ZD+LZm$O#1D|~d!5}Dcf{5g;46(3p&R8qroODI$s1+OoI!}+@ z_Ior`RP=aAVGl2zpUW@U-R)*u!<`>C=-aH>ZS7n7f3r|=hCL`AQNW7PO)LJ~D1B+l zhk(eD7ZXV^WF5k!@)#Td+KMlE6pU#ll~=YBDlT#!A)k`JwXs>Yd8^k)BM2G8r>1ot zs-e~6W4Be5aXnNulQJCZxLzYa=b~%}AsIL2Br9c&X*u{y3?bWwW0$@Xc3Osg&)?>m zcC+0?66!skL0Y!NE=pzrY{x} z%ZpHjjxVjEAERql?@Kg6IynYX?q(I(Mz>#lNWVZ}P72PVP~Y%8E!$QCUIb?e-p7;m zd+f42|2afA0t!izbZNiuV^E-%U~So+*omt2n4s#)5)8z|KD2Tov4>qN>Md(8 z9C<4*ktH{YW^+%8s8SO3SV=>YVZhl#&fb1wJn%p$H5{c8gZL#!Y)7o!VkGO!EdG40 zqQQt2)AY6>H+k5_Gu#A~Sa9#$=t<(x%H&kGySGB+YM}4)(K-5eZ#f2&Q{%r8{c~0+ z25p%Te8d&WLYqvn!JrW~Q~J;OO0~ANm-l9e-AqY;oRQ6DBNTORxNjF?3rSQgZRqOh zO*7vSh*3}+%ST|SsjD~QFR$9(`C6hDv)2Eg0XEJ2a*rK=Pj*KB#nQE|yB#88qnu@^ z%yNdd6cP14E0uz3k4*+4lJ5x*=S%G63%hJJjFjPSX(J(<%#FL(TruhB?R0hHGbWb< zFqdx{eMw47>;AkiDdG4bH{t2|_z93!5SgQ9FC9ET438%{;T2G_n!nukY&#M3Lktp^ zAmTROAMhrH{e90})adii9x~SP|2@OvEa-Af%OVl$Q(-zUC$a-^^d7IJH3S4-GPlR~ z#Ok~+)U$YVPKV$?bb1Hlk?K&gcl?r78)|%5lYv=Z;#_7&g;==HS|LqL%teA$tKr`rNIE?pBBPy zrCOCO(Mxu3wU3zh0k6{Q@gd7+<7&?xqm4#|1uIzCG#kM-w}i(dMWLy#{Y>ej<6}V* znfFiAU4Iqo)O?F=Jb7_w$@RkJ=R$zhs3;Z@I(KxIsbyORGziXYP-G$XLde|ygM%sE z^Gm1~1>*WbOtM-9(EQzu`5gmJ{H_OX|v zMJ?y%WKD<{_%cM4(pcI_)`PQ@@LhD&F3KDBe_TU5_vLG1`ZGkls0-|d>om>g?oTa! zz!vh3^)t8Cjps<kr+(7?JZmC032; zI{HN2M_#Qsq~F41kj2=YHv17Y=ny3f8ItQ=NmXW|?&^P6-iy(C+EV{;T|eH$mah#9 zcL<&%>pFDLV!10-?J#F-@c#SPiU$OCGQFo-irjjU0 z#~ar-co<doc9VAvis7C?6lCwEoe3WzHA24RkDEMpl97RikG%1eQ*n$F<=q*LZ z;kOkd%yeoauGG?oMM{Kq3koBXHO-rTi4gn0JI%#K_CIH@E{~2F{I)(FV7;F9AYyLE zvU)x$2_xBNZ>ah@eI)-xAL2L?FDT*%*3{MEKOsU)bVemC}E7MG@-#Wt4gONb`9ZdZ?HPs3R8?e*)oaJ{Df zkVN4uNL5GO!mMzM<`v*nu@L!H8-iVy_gz1tsBY5JZkBw$i;ora!M;C%cH4U*kN3-S zM>j+4z+Roj`JW*he_5eRtuY4VvF_%fr++uiog1;1I8dS(ddq%8>_FfTw|!Ft zVr{5VSsS6ww2C-BQgg=a!z%gCghfGyHZ_h|87@&LpxzkzxDrErvQNW{s;v7Dq8M!T`l8r$Adg7%@i=#7iJ zxBiM6;7zYZo#s5^DzJkt%sgbjvL{oxBuJcm@UNVrFU3BE{I94eVo&(Bz1i=sVH6eR zeg6FSIG$a-=_TkH0RXY$4h+osjjw%=yk_mU*E+&lj#T5jp;o4-oRRuCPF!rZj7a)o z+`v1CI#&@PNQsNMee}b35>Rk21n46wN)h+D3TTDw?%Qe(-?+zry?y?%^J&nQ}IGF_GGvI-W$5=hokN?pLS>NeC2f& z#FbypHa1{yZR-<{XCoclTeFI}yg6u)#`7VIS`0&z!g{M&V}46!r{Ul>iBG(|3U*Lz zt+wReLl>wj_^|LKjMLHq}W{tT5JYq1jn zQ?tiN)H|g}YR!Vg)6LxmajXK$?(S}f0m;QEkBXQzHBQV7X+=S~4AF+y3_f;X{Z;KK zAHp7VFVfDI^j?4X{Tr6r=WEC@P~sv+GyBF^f-BlsAxAr6nL9#hO(#FEC(V^&TK1M2^ZIVGu3 z9wWvVw?{7g6UMGv&jwp@SU)DzPbL}QyWY^Y6v{o4KiBE7CT{xS_^8?3{|vm1rcb7V zDqmRXw^{0krTOkXpf(EV$LJ?JA_m>5oCYbjC}A=99iGYw&dhSibcQaE~k=|CK2GS@=aBd4>$lU1|?+18r)h z6#P1jigqKmVdf-Q>pW!gmuewkgvGMhl6h%T`ix94tn&fy?s}_(j=CyWfg6w^Y!Ys2 zo@xvWO{V+r8I{BbL&)&&*7(83=g*(rQWxte1TZxj-^oeGaxC13-tM$YP*AXLlbS$Q zarjXwO}8JVQftIAPx5rYtY7|U=|fKha|c7l9#4pa&Lz!wK$;BVe$O~#V1Lo3a%`GK zyDX!$)Ra$uH(S-^!Y{jyeba>Vw zqqmo)|F9! z1oBrh7UZR^KHj9pe716M@(UvHTWA}eg?yB=^beLYSy3Mg(akZrBZk>oe;oq$_{t&~MB*v>>d|Y<~1wu#(AchRFC|+pMD_ojC`q0_n|Uh;gLX`aM9s=HsR!dkO41!&nC6ekKUQ7^lCw! zTvYy8PUK6(-Euu_aIzw#c zenDQTQFN`EKD^%R7={aoJ8kI`>lNq<8yT3F%8?_IWU<|62E5H#0|EIsSlzO}u&{0X zLpcAt6SylpxS^AOug|=}YtL6-UyMnT(d@GIdDBk77AAWUIK9Qk^W`n52kxWhxB$9wBaP%EAPK*Z&>q~3VPwrbVvtE5 zOQmqd{{yX`1=!;wxtd`2gi{{r^d5a$Bp&$$@x^c5xQ@;>_N zY+G*Ds0&fONfQ1W9=#Furx)WEX$8qOOV95|Y zN2rG%dg-sgmWB}}AP$ChF_!(7H^V2FuOB*~orDALSl^MM8! zFvakmnt^v^$v61+TL9HjQPoua{h)M)pq1-G=1+gHs2})AGnNy@VzZ}K;;@Iwp11xO zf6Xjoo*K;Dyjl0GT*42cfZ70$6X(89iG$9%)!-W*D@6h)YTzb)E`zauaddR2R6B}z zMk3M^`^C3oI|hX6lz7_Y%78nj^EJrO4Ld=6c34|%IZ;y6wJ>pmIYFZ(gjVP`_7?=E zJuR(*!1a(2pLom=Yz@W0rI>@JtIIuz$~+^H-+#n@G@q))nogRGnh5;uwLsqsBsk`F z1upj(a7Yh<&BqpD!T2AWg~|Iw?51iLb*p#I@8#%nH+gk9|C5`tm@ge-vnZ1`d*F7g z4b91oC>I^vV8rg9M#)LW(h^c3#6H$uexgAh>bJnS@WAP=A~vGy^5RT=wJ)RQWk&>+ z1lV831jNL~O&M>98NUU2Oh>QZp)tZUl2KC$yCC(VjA!bh@e(BvO2rgv*%8a?H-K>O9=x$$7U(o< z77-JBYHO@v*%MWU%fH`St-b^RP|vmp#}%3d7{eLSc^v)y=rW@d$lz=If_e$`C&L52B}#PjOXRPb?X+-?c35#AhG6fXr>DtWqHn|nL4ix>U5dD z9N!vvd9-(@IceW$%8@_J_A&BONs;}!%(tF?8Y#TA-X*G$$8^6S2<#*Ux#rP-JG0H@ zvy8Ku`9($Vry!$|50}8DZ*Q^Fre7{Eqr#Mtt%VpUK~jRbZkExbbL;u*QLAyfdfs?BGCU);gB4(EKIWl}>3>lnSjE+v42< zk7)9ZA4xn%=058Mes?upzn`%SX{y(*O`t4)r;%$_rvT z_ZNjuuL*cqm|PtajWHENnfMfNcD4^r27%C^4U&+=NT<5!jcB;F()HiK_DjtqP9i&k zCmV#17L~LL(A8*=t4&kYuA@!1Ies2xvpX~i?qMZO8j}MQ7-ox% zT;a<3uCHJSIrfEq;h-cg?EG9fpm}cYV^L8Fow%D_fUb7;Y(heU1Y~y_HX(^m?cHcr zPpZrteKGM5^V3ocKQnASI4yhdVJ>GO?tMnLF!mZo;wdSH=V&kfdvKl-uN?c0Kj*_e z3>$xG(=}_osJbB`Rj$!bX6tP>JkaxqWwS%vf-*ctFK441 zX}?RZ@5@#6ga7c~a6z zo-ay+bpP2Pk&#$%c-&h-i2&!705Fw1Z3K8+rEKIke}SZ*A7pu+J*Mxny1UqOe%7HO zttp-U`7Qb1hwed~i&ez(d3R-d*_rB$4KQ}WL%3S3f0tY{vT^5x2jCb_o~(3$O5uz5 z>JDL~p+!}4*R|aaWehRh%&3r-4SM*`DhJItnA6-|2vR z(`1KFjte%Zfl0^aZP&iH(~_{W(ofZQwPlUB+(-V=CGPfc#k8!_W+Te!Q;wo`hmZKK z;dFzC;?ZrxRj)ITFrcZt!s35+u-)dRbWKLVXS`(g-PQ$2#dqrpJDSG41)XH$Sos|c zp-LB8iYzReSQr4L(q}YQ$uOu#ImX#hwnVZ$5=ZucifC)XfqnD!r}IE*J#@c$Abs(7 zxB%*W*GA!VB*aUSsc2{zLygCIA6nx`95XQ*@22BxlAHv7XGNeIpWSjIq`ZYI$&Bp4 zmMQq$zZ`Fm1%HgUcWlPRB_s|n@#s*n0Lmq=7t(#N#x1tp|MsYzj7=n7AM2*1>U|g2oNcyb+o40#IeYD8_Y> zme?RaJn1V8iva@>+^(>=-%X~qw*@i{>rXd|>jMtQZOzfq8{16vzFA_VrJrcRE#!|C zhduh>Cl+m{L_r{!R%#43n5haW^-K-f#Q7PlG~J9mmt9;#l*q1VavXeGt@)OC8w7x- z%rE%^Dt%=`lPFV!d7SaMZxpP&)XQoEVF}HfTSZ%Gpzejew?y!SHHEC{;8$R(|}XJnXLlQld1 zy_XydygL-s0(shpjp?6z`Q-KH5sG1osvX;NDSf@YB{ZIMM$q7CJ39VxK${~|Xw1AG z<6+AiGLK&CQcHX&M&8{X=oL1bK7^D0CA-c)bWU(61%?OB3qE;+l>2{gCVErv@7x%` z(P2obR{y?HWt?4mP=`_IHz1n-8-|QC$KMz)cT0PgH{`fv2425BNt3?5^C^4|^2y#`Qni1S$1dR|BsF#pg>`Mvb^)3RFe;jR`d3sLtIGU+rsZqz&2 z8QmU$fR<6ncn`TFN&ZuWg>sOq)C{`gZ@^!F=&?K3Di~6gT-id#^E&?3{oCWq?)Se| zQ{h=~PYi)sDzW}g-KHl5#sa7pzE-*Pm(--%FTVQwKEtW5x-xHH6r~$7<>yVoq<0)X z<5zjr3Ed4J^a^ zKK!Qe`VBE(>t&dtMKitoN3(e1-9{|*xMpTTR_`Vv?`E2Q;I#v~`ooE517Ei)d$OMc zWE=Me$^Qth<}ZH39n45et95~CPzR{Of>qaJyaX-L0Nx&#cS>xMmQkTp`;jLn!COs5 zMP*Q;Cfa6uuhdC6HZS&oa3KffiHOInItpzPLtjjiu4$kAj{~D6Z$BDgusluakTXh4 zO8UKUx>?SM4mXfNIY+Z5j)RXw5aturC{h}Ty2}~6C1B*<)obxeO%5-eIgX;uXgHVD z9JN|A|B$P|S@g39bTT=j#|^DA43f^y69)q_G9O_>r^MB%5_C6tnp^s)#A@A>hsNf* z3;#M7rIHR7hS4SZhV_D{CA!Nl&LwWVJon?Axsj^26;Ahq0OV zu|-9xy>$5Fcva0e;;T!vGo+a9xVKWHLX%D-a{wVS4l?U3ujcTJ|LTWaa82&t-7V%Qbi+`Q^!!BAXTpooxMfdboX-2o1Bs@O4A7$7n3(<>gUx zOkQGTtoh8fR2rOP$xDuF9D@%;5oFd?O_&HYEZ`f-Yt|ZO*A|gq600jKGZ&}N$}b8& zmu#IuLDy$q2`m~2cRF&&EAq$3^HV!~&YF?E$xz`y*fLsf-aLhNyqZJTH~?YzuMdVl zm#iFX!uuzpsOZ<~!T9)rwuv1fHfpTo8?KeUyS~701vAsYt(}U`ls}P%{XYWsKD-kC z_)e9&W;w=KD*2~{nYTddPIvbZ-skP0? z`E&65flExqj|AGt5^ioHn+d;}^EID8C#$!+HxoS*Cby-pG0`!9zx&{Cyrr)MfggK* zaq6vT?16diDx8k$`!bjzsnV8(Jqj`Wei0RXT;fPF@&=#~fBK^!^l?_3&W^Aoooch2 z=Q1disU9S(g*4mj>nVwel-Y!tKUXd~ctcFck(NB%Jcc=- zHmC4427&ZOMy?*bC=>VEog0A&OMf5mwi01^N=Qlh<-#YVmpzaCe9vJqvV+zx^S^sY zd<70Rpyua;bIU|>U|H1rBBt_RG;yxQwBnC3UH`A?}DT4*0B7%n0{sJ5uY5o9u zJ6s1cat^%7zra*w8~n)0z(n?_pH_{?!^Yz}n0AWZ`gmE&+PYU-l4D0%qO;xhhJ5$_poW+X#P`vpwJ87Et#A(1%s}HfAgvWx#{8X3T68zkN648Vs3s_+`3z3e_^$RCBNmAHG8XveW@ctED#`QOnUOk6 z2H(3kgtKJ6rOq4s2MDac<{f`sB5fUn_@(e)3Gi0sxmhF+@Ru&OPdFRqq#oT)kpQDy zQhNGEe^=LTrpx#1+h~FL6FdYC5A;L-jJ};zr0CpwL+yEVH)ArLWI{?*H(xQq&Mg9) zTSClihjG~tpV?XT#u%*6`Ssxs<)>hjBw+*#HY8m3e*0XHPaz^Q|7~b})se>^Hk@%+ zT*w`hshIfNz&S$*a+Z7U-rp*k#XSsJuzMPP0Lqjnc$fV)VVARp_;D8TRBJf~rIH*t zg8qSaKqP&6yjTEG22T8Qww3rLy#N807TcpF{l9M#=cVjT_)uo(6_DjpW&3lDp*Uag$p9)0w( zPYh^}>kw0D?UH$tjVWN5`=ljC+&K6Q8Ar^ifbM1Joqy-``jfL+fyiI=A} zX%x#_RiNT5B!TIFbL;5Lg6}Bf+S9yG{FXv_&Wzuu@NRiNih6hQoK8Gu^D!Y|C?`{h z1u%GU;Ema14kv~DL1S%j4f{)=6ix%t%0ERT%Xd{c$sjy2V* zb~zRb4(mIwpAI7ByFgeWxprM}Nl0HmL%uAuD@z04pzP7fF#0XN-qF!vQ&)DN!p4%s zh|(T)H;0+bzaShUczagbppTsaX~HhffKhP=P%*TIW@i4FEBn(1H&8}hdIl|e#9QGH z+7I3-!CIg|w3uACOTty3qK?ZRrP*zc^q$RsOw$plfQSXTbK|YT>q8Y#hKK~GJ#Giw z40}Ekd1{~|8H}6*)M(lf5U9!(@w=6*7vj`Hu#GGfgEQ+D5emqOLCA) zbg?!@G0A#q@pCD|pY-!!Uwl<3`CsQRx0kLA>Q-|2vXtb5EeRI%3Xw$e5?{WE{1HNe z<-_)qEs1J8Qc}~u z%V&*kf4ySA37Pf>Fw^>KH`JyiGXOc|gSQGeN{|X3`0FKU(xCAs1SxrWJlpBrIP)R| zuiYqZ$olR<(48%~wpt}%8d*hQVAa(MXO?zr4VWVE#rCQXxLa?Q%p^EsVU<8<1Z?i$A3AU0mkjHm39VNXX>TCxZ_W>w#_4a_% zcny|sLp0Yi2+ZhZM<>2i5c%(VQ9s>Z=avok1jan^fQ50C3u|0qT~g%uZ>aX3TQ&iC z_s^fYPT=Z&3s~vD2KMW+viPmgm3@8zj*4ccsmXyN&7rTmZI-heW2&TmNr*AIl6JZG z7u5>>9vUm33K91&{AJKEmPj|#8jbSSs>by9kKx&UjgI%#qFXpuG({q{c;pnZ23k$e zJPr}{^WPUf-Zg)w#gX!1^eL^Q@^d*mrF%XyP@QwZl4r+frfwdq?fa3L@AXarJ{tkT z#!qW!c)I*d&W3<&t^xRzCqjC zuV>&9U1+p4K=G_~Z`6zIg@~{c1vRKC$s#vDckX_vtE;mI$iMnc&GgR%^QX9R!}lR^ z*I@4dZ+XI7rg!oX+#r|25)u*_$XXNrOkrBQXa=$LoSd8h>=olA!=iBJPbJqHJ|=p` ztYm#SM@XUxLlfxh=m_gD`{SBzNQ@GJoW70CI^3Ki7RfPXHbp_#oUB{3j=tyI^VVz# z1B+5uqFLJz6q6)%W)@zTAKn85`X~$Z!3Kx2Xkrp%By_)e?JfL94`D~3y^3hLu;D61 z+(~(hF$kN7!N1YwOZO?DolICk_xTOR)4F+|I-jM#Qiau?Rkq2CmmP%&!x1<>da|<- zDu=Kx`0R!_g+T`OjK`z{Jpr>$BqkyASd+t9dP|i}n3MhkeZ|Z6C`O#~bbJ)Cb=QEC zUI?YHuMO?1sJr~Od?eyXsN0{b!%KrPTj@jM2D}@T!AvRY3DKFCn6Enu6~5f+xBj`> znFoqH{(*G{=r%9>g(084WjU0@Z0K-H+&p9>567%)pd@1SX> zeKlF0S}v2Gd2ud|Asjo0*pf>wZ0G(iThh;(PJKP-;OR$25+=I;8oF=MJXvU!3Dyqn zkWqMbY$|td^Im>oM45qlT?^-VNQUA-Y+bJ;u5E~JB_f)X#w(HMzb2`E^DOca7OU*FB>+C zd-70gg)ENyr^RcBSokASYq2CSTmB4ur9Nbs>o1?5U|ZBsEXYand=kXjjv&B9O2N5Y zWU@ln_E#(A`eJ9J5n+eV)1LF_$w#!K1_HY%5H2@YT$jVeag3L4V8szt?&mHMAaZ<4 z)1UF|{Aa=bK5VNgNvTA;?rI1AGZVXVwrFS6oL?nczL%`*GzC$ zdOvMFy7ZJ@j9oM4to&hchtJ}R=x&6HQsphc#CJYU+oF82L^Df<4~Je>)Ajn0;K@%R zXFV?c^Q0A*w6>mHVK0o5!%0Sm7QigCUW_yMC{@8nt>L3c?kMN7WDf2!^*AtTz977_ zKiQ$bt0?=xKSHq+&->IPPMwURb|U;HL0`ZP_{;L}(v(p2c)fuNQT)3qJlUinq)&?F zZrxGd#~(lv$J<6cujQTV8p!&O;H0BzZZ>pw1TrfIA!^TQ_{zen0^WULXTnuE#%L#L zx1{G%5FiyyOEXuFuv?4YSUD@4VN~hdx2HsG8EWr7&B1_A zbxk0u{bPHEf-347d{SR#hN^&uY+DT^LH`GIjA>*5 literal 0 HcmV?d00001 diff --git a/src/main/resources/static/load.js b/src/main/resources/static/load.js new file mode 100644 index 0000000..264148f --- /dev/null +++ b/src/main/resources/static/load.js @@ -0,0 +1,54 @@ +function loadHeader(partyId) { + const title = $("#navbar-title"); + + // because I don't know how to handle relative url if current does not end with slash + title.attr("href", `/party/${partyId}`); + $("#navbar-bis").attr("href", `/party/${partyId}/bis`); + $("#navbar-loot").attr("href", `/party/${partyId}/loot`); + $("#navbar-users").attr("href", `/party/${partyId}/users`); + + $.ajax({ + url: `/api/v1/party/${partyId}/description`, + type: "GET", + dataType: "json", + success: function (resp) { + title.text(safe(resp.partyAlias || partyId)); + }, + error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); }, + }); +} + +function loadTypes(url, selector) { + $.ajax({ + url: url, + type: "GET", + dataType: "json", + success: function (data) { + const options = data.map(function (name) { + const option = document.createElement("option"); + option.value = name; + option.innerText = name; + return option; + }); + selector.empty().append(options); + }, + error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); }, + }); +} + +function setupFormClear(dialog, reset) { + dialog.on("shown.bs.modal", function () { + $(this).find("form").trigger("reset"); + $(this).find("table").bootstrapTable("removeAll"); + if (reset) { + reset(); + } + }); +} + +function setupRemoveButton(table, removeButton) { + table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table", + function () { + removeButton.prop("disabled", !table.bootstrapTable("getSelections").length); + }); +} diff --git a/src/main/resources/static/styles.css b/src/main/resources/static/styles.css index a24324d..e69de29 100644 --- a/src/main/resources/static/styles.css +++ b/src/main/resources/static/styles.css @@ -1,277 +0,0 @@ -/* in-text images */ -figure.img { - float: right; - border: 0px solid #333; - padding: 0px; - margin: 5px 0px 5px 10px; -} -figure.img img { - max-width: 100%; - height: auto; -} -figure.img figcaption { - margin: 0px; - font-size: 90%; - font-style: italic; - text-align: center; -} - -h1 .octicon-link, h2 .octicon-link, h3 .octicon-link, h4 .octicon-link, h5 .octicon-link, h6 .octicon-link { - display: none; - color: #222222; - vertical-align: middle; -} - -h1:hover .anchor, h2:hover .anchor, h3:hover .anchor, h4:hover .anchor, h5:hover .anchor, h6:hover .anchor{ - padding-left: 8px; - margin-left: -24px; - text-decoration: none; -} - -h1:hover .anchor .octicon-link, h2:hover .anchor .octicon-link, h3:hover .anchor .octicon-link, h4:hover .anchor .octicon-link, h5:hover .anchor .octicon-link, h6:hover .anchor .octicon-link { - display: inline-block; -} - -body { - padding: 50px; - font: 14px/1.5 "Liberation Sans", Helvetica, Arial, sans-serif; - color: #555555; - background: #eaeaea -} - -h1, h2, h3, h4, h5, h6 { - color: #222222; - margin: 0 0 20px; -} - -p, ul, ol, table, pre, dl { - margin: 0 0 20px; - text-align: justify; -} - -h1, h2, h3 { - line-height: 1.1; -} - -h1 { - font-size: 28px; -} - -h2 { - color: #393939; -} - -h3, h4, h5, h6 { - color: #494949; -} - -a { - color: #3399cc; - font-weight: 350; - text-decoration: none; -} - -a small { - font-size: 11px; - color: #777777; - margin-top: -0.6em; - display: block; -} - -.wrapper { - width: 80%; - margin: 0 auto; -} - -blockquote { - border-left: 1px solid #ffffff; - margin: 0; - padding: 0 0 0 20px; - font-style: italic; -} - -code, pre { - font-family: "Liberation Mono", Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal; - color: #222222; - font-size: 12px; -} - -pre { - padding: 8px 15px; - border-radius: 5px; - border: 1px solid #e5e5e5; - overflow-x: auto; - overflow-y: auto; -} - -input, select{ - box-sizing: border-box; -} - -table { - width: 100%; - border-collapse: collapse; -} - -th, td { - padding: 5px 10px; - border-bottom: 1px solid #ffffff; -} - -td { - text-align: justify; -} - -dt { - color: #444444; - font-weight: 700; -} - -th { - text-align: left; - color: #444444; -} - -img { - max-width: 100%; -} - -header { - width: 20%; - float: left; - position: fixed; -} - -header ul { - list-style: none; - height: 40px; - padding: 0; - background: #eeeeee; - border-radius: 5px; - border: 1px solid #d2d2d2; - box-shadow: inset #fff 0 1px 0, inset rgba(0,0,0,0.03) 0 -1px 0; - width: 15%; -} - -header li { - width: 8%; - float: left; - border-right: 1px solid #d2d2d2; - height: 40px; -} - -header ul a { - line-height: 1; - font-size: 11px; - color: #999999; - display: block; - text-align: center; - padding-top: 6px; - height: 40px; -} - -strong { - color: #222222; - font-weight: 700; -} - -header ul li + li { - width: 8%; - border-left: 1px solid #ffffff; -} - -header ul li + li + li { - width: 8%; - border-right: none; -} - -header ul a strong { - font-size: 14px; - display: block; - color: #222222; -} - -section { - width: 70%; - float: right; - padding-bottom: 50px; -} - -small { - font-size: 11px; -} - -hr { - border: 0; - background: #ffffff; - height: 1px; - margin: 0 0 20px; -} - -footer { - width: 20%; - float: left; - position: fixed; - bottom: 50px; -} - -@media print, screen and (max-width: 960px) { - div.wrapper { - width: auto; - margin: 0; - } - header, section, footer { - float: none; - position: static; - width: auto; - } - header { - padding-right: 320px; - } - section { - border: 1px solid #e5e5e5; - border-width: 1px 0; - padding: 20px 0; - margin: 0 0 20px; - } - header a small { - display: inline; - } - header ul { - position: absolute; - right: 50px; - top: 52px; - } -} - -@media print, screen and (max-width: 720px) { - body { - word-wrap: break-word; - } - header { - padding: 0; - } - header ul, header p.view { - position: static; - } - pre, code { - word-wrap: normal; - } -} - -@media print, screen and (max-width: 480px) { - body { - padding: 15px; - } - header ul { - display: none; - } -} - -@media print { - body { - padding: 0.4in; - font-size: 12pt; - color: #444444; - } -} diff --git a/src/main/resources/static/table_export.js b/src/main/resources/static/table_export.js deleted file mode 100644 index ea019df..0000000 --- a/src/main/resources/static/table_export.js +++ /dev/null @@ -1,31 +0,0 @@ -function downloadCsv(csv, filename) { - var csvFile = new Blob([csv], {"type": "text/csv"}); - - var downloadLink = document.createElement("a"); - downloadLink.download = filename; - downloadLink.href = window.URL.createObjectURL(csvFile); - downloadLink.style.display = "none"; - - document.body.appendChild(downloadLink); - downloadLink.click(); -} - -function exportTableToCsv(filename) { - var table = document.getElementById("result"); - var rows = table.getElementsByTagName("tr"); - - var csv = []; - for (var i = 0; i < rows.length; i++) { - if (rows[i].style.display === "none") - continue; - var cols = rows[i].querySelectorAll("td, th"); - - var row = []; - for (var j = 0; j < cols.length; j++) - row.push(cols[j].innerText); - - csv.push(row.join(",")); - } - - downloadCsv(csv.join("\n"), filename); -} diff --git a/src/main/resources/static/table_search.js b/src/main/resources/static/table_search.js deleted file mode 100644 index a2eec3b..0000000 --- a/src/main/resources/static/table_search.js +++ /dev/null @@ -1,21 +0,0 @@ -function searchTable() { - var input = document.getElementById("search"); - var filter = input.value.toLowerCase(); - var table = document.getElementById("result"); - var tr = table.getElementsByTagName("tr"); - - // from 1 coz of header - for (var i = 1; i < tr.length; i++) { - var td = tr[i].getElementsByClassName("include_search"); - var display = "none"; - for (var j = 0; j < td.length; j++) { - if (td[j].tagName.toLowerCase() === "td") { - if (td[j].innerHTML.toLowerCase().indexOf(filter) > -1) { - display = ""; - break; - } - } - } - tr[i].style.display = display; - } -} diff --git a/src/main/resources/static/utils.js b/src/main/resources/static/utils.js new file mode 100644 index 0000000..63bc91c --- /dev/null +++ b/src/main/resources/static/utils.js @@ -0,0 +1,44 @@ +function createAlert(message, placeholder) { + const wrapper = document.createElement('div'); + wrapper.innerHTML = ``; + placeholder.append(wrapper); +} + +function formatPlayerId(obj) { + return `${obj.nick} (${obj.job})`; +} + +function getCurrentOption(select) { + return select.find(":selected")[0]; +} + +function getPartyId() { + const request = new XMLHttpRequest(); + request.open("HEAD", document.location, false); + request.send(null); + + // tuple lol + return [ + request.getResponseHeader("X-Party-Id"), + request.getResponseHeader("X-User-Permission") === "get", + ] +} + +function requestAlert(jqXHR, errorThrown) { + let message; + try { + message = $.parseJSON(jqXHR.responseText).message; + } catch (_) { + message = errorThrown; + } + const alert = $("#alert-placeholder"); + createAlert(`Error during request: ${message}`, alert); +} + +function safe(string) { + return String(string) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} diff --git a/src/main/resources/swagger-info/description.md b/src/main/resources/swagger-info/description.md index 39647dc..a2b184f 100644 --- a/src/main/resources/swagger-info/description.md +++ b/src/main/resources/swagger-info/description.md @@ -11,6 +11,8 @@ REST json API description to interact with FFXIVBiS service. # Limitations +No limitations for the API so far. + # Authentication For the most party utils service requires user to be authenticated. User permission can be one of `get`, `post` or `admin`. diff --git a/src/main/scala/me/arcanis/ffxivbis/Application.scala b/src/main/scala/me/arcanis/ffxivbis/Application.scala index d91737c..951b91d 100644 --- a/src/main/scala/me/arcanis/ffxivbis/Application.scala +++ b/src/main/scala/me/arcanis/ffxivbis/Application.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -8,16 +8,16 @@ */ package me.arcanis.ffxivbis -import akka.actor.typed.{Behavior, PostStop, Signal} import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors} +import akka.actor.typed.{Behavior, PostStop, Signal} import akka.http.scaladsl.Http import akka.http.scaladsl.server.Route import akka.stream.Materializer import com.typesafe.scalalogging.StrictLogging import me.arcanis.ffxivbis.http.RootEndpoint +import me.arcanis.ffxivbis.service.PartyService import me.arcanis.ffxivbis.service.bis.BisProvider import me.arcanis.ffxivbis.service.database.Database -import me.arcanis.ffxivbis.service.PartyService import me.arcanis.ffxivbis.storage.Migration import scala.concurrent.ExecutionContext diff --git a/src/main/scala/me/arcanis/ffxivbis/Configuration.scala b/src/main/scala/me/arcanis/ffxivbis/Configuration.scala new file mode 100644 index 0000000..f92bf8e --- /dev/null +++ b/src/main/scala/me/arcanis/ffxivbis/Configuration.scala @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Evgeniy Alekseev. + * + * This file is part of ffxivbis + * (see https://github.com/arcan1s/ffxivbis). + * + * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause + */ +package me.arcanis.ffxivbis + +import com.typesafe.config.{Config, ConfigFactory, ConfigValueFactory} + +object Configuration { + + def load(): Config = { + val root = ConfigFactory.load() + root + .withValue( + "akka.http.server.transparent-head-requests", + ConfigValueFactory.fromAnyRef(root.getBoolean("me.arcanis.ffxivbis.web.enable-head-requests")) + ) + } +} diff --git a/src/main/scala/me/arcanis/ffxivbis/ffxivbis.scala b/src/main/scala/me/arcanis/ffxivbis/ffxivbis.scala index 6ad9732..15db400 100644 --- a/src/main/scala/me/arcanis/ffxivbis/ffxivbis.scala +++ b/src/main/scala/me/arcanis/ffxivbis/ffxivbis.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -9,12 +9,9 @@ package me.arcanis.ffxivbis import akka.actor.typed.ActorSystem -import com.typesafe.config.ConfigFactory object ffxivbis { - def main(args: Array[String]): Unit = { - val config = ConfigFactory.load() - ActorSystem[Nothing](Application(), "ffxivbis", config) - } + def main(args: Array[String]): Unit = + ActorSystem[Nothing](Application(), "ffxivbis", Configuration.load()) } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/Authorization.scala b/src/main/scala/me/arcanis/ffxivbis/http/Authorization.scala index 356887d..971b869 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/Authorization.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/Authorization.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -8,22 +8,18 @@ */ package me.arcanis.ffxivbis.http -import akka.actor.typed.scaladsl.AskPattern.Askable -import akka.actor.typed.{ActorRef, Scheduler} import akka.http.scaladsl.model.headers._ import akka.http.scaladsl.server.AuthenticationFailedRejection._ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server._ -import akka.util.Timeout -import me.arcanis.ffxivbis.messages.{GetUser, Message} -import me.arcanis.ffxivbis.models.Permission +import me.arcanis.ffxivbis.models.{Permission, User} import scala.concurrent.{ExecutionContext, Future} // idea comes from https://synkre.com/bcrypt-for-akka-http-password-encryption/ trait Authorization { - def storage: ActorRef[Message] + def auth: AuthorizationProvider def authenticateBasicBCrypt[T](realm: String, authenticate: (String, String) => Future[Option[T]]): Directive1[T] = { def challenge = HttpChallenges.basic(realm) @@ -38,34 +34,26 @@ trait Authorization { } } - def authenticator(scope: Permission.Value, partyId: String)(username: String, password: String)(implicit - executionContext: ExecutionContext, - timeout: Timeout, - scheduler: Scheduler - ): Future[Option[String]] = - storage.ask(GetUser(partyId, username, _)).map { - case Some(user) if user.verify(password) && user.verityScope(scope) => Some(username) - case _ => None - } - def authAdmin(partyId: String)(username: String, password: String)(implicit - executionContext: ExecutionContext, - timeout: Timeout, - scheduler: Scheduler - ): Future[Option[String]] = + executionContext: ExecutionContext + ): Future[Option[User]] = authenticator(Permission.admin, partyId)(username, password) def authGet(partyId: String)(username: String, password: String)(implicit - executionContext: ExecutionContext, - timeout: Timeout, - scheduler: Scheduler - ): Future[Option[String]] = + executionContext: ExecutionContext + ): Future[Option[User]] = authenticator(Permission.get, partyId)(username, password) def authPost(partyId: String)(username: String, password: String)(implicit - executionContext: ExecutionContext, - timeout: Timeout, - scheduler: Scheduler - ): Future[Option[String]] = + executionContext: ExecutionContext + ): Future[Option[User]] = authenticator(Permission.post, partyId)(username, password) + + private def authenticator(scope: Permission.Value, partyId: String)(username: String, password: String)(implicit + executionContext: ExecutionContext + ): Future[Option[User]] = + auth.get(partyId, username).map { + case Some(user) if user.verify(password) && user.verityScope(scope) => Some(user) + case _ => None + } } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/AuthorizationProvider.scala b/src/main/scala/me/arcanis/ffxivbis/http/AuthorizationProvider.scala new file mode 100644 index 0000000..46cf1ff --- /dev/null +++ b/src/main/scala/me/arcanis/ffxivbis/http/AuthorizationProvider.scala @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2019-2022 Evgeniy Alekseev. + * + * This file is part of ffxivbis + * (see https://github.com/arcan1s/ffxivbis). + * + * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause + */ +package me.arcanis.ffxivbis.http + +import akka.actor.typed.scaladsl.AskPattern.Askable +import akka.actor.typed.{ActorRef, Scheduler} +import akka.util.Timeout +import com.google.common.cache.{CacheBuilder, CacheLoader, LoadingCache} +import com.typesafe.config.Config +import me.arcanis.ffxivbis.messages.{GetUser, Message} +import me.arcanis.ffxivbis.models.User + +import java.util.concurrent.TimeUnit +import scala.concurrent.Future + +trait AuthorizationProvider { + + def get(partyId: String, username: String): Future[Option[User]] +} + +object AuthorizationProvider { + + def apply(config: Config, storage: ActorRef[Message], timeout: Timeout, scheduler: Scheduler): AuthorizationProvider = + new AuthorizationProvider { + private val cacheSize = config.getInt("me.arcanis.ffxivbis.web.authorization-cache.cache-size") + private val cacheTimeout = + config.getDuration("me.arcanis.ffxivbis.web.authorization-cache.cache-timeout", TimeUnit.MILLISECONDS) + + private val cache: LoadingCache[(String, String), Future[Option[User]]] = CacheBuilder + .newBuilder() + .expireAfterWrite(cacheTimeout, TimeUnit.MILLISECONDS) + .maximumSize(cacheSize) + .build( + new CacheLoader[(String, String), Future[Option[User]]] { + override def load(key: (String, String)): Future[Option[User]] = { + val (partyId, username) = key + storage.ask(GetUser(partyId, username, _))(timeout, scheduler) + } + } + ) + + override def get(partyId: String, username: String): Future[Option[User]] = + cache.get((partyId, username)) + } +} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/HttpLog.scala b/src/main/scala/me/arcanis/ffxivbis/http/HttpLog.scala new file mode 100644 index 0000000..0e8b4b4 --- /dev/null +++ b/src/main/scala/me/arcanis/ffxivbis/http/HttpLog.scala @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2019-2022 Evgeniy Alekseev. + * + * This file is part of ffxivbis + * (see https://github.com/arcan1s/ffxivbis). + * + * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause + */ +package me.arcanis.ffxivbis.http + +import akka.http.scaladsl.model.headers.{`User-Agent`, Authorization, BasicHttpCredentials, Referer} +import akka.http.scaladsl.server.Directive0 +import akka.http.scaladsl.server.Directives.{extractClientIP, extractRequestContext, mapResponse, optionalHeaderValueByType} +import com.typesafe.scalalogging.Logger + +import java.time.{Instant, ZoneId} +import java.time.format.DateTimeFormatter +import java.util.Locale + +trait HttpLog { + + private val httpLogger = Logger("http") + + def withHttpLog: Directive0 = + extractRequestContext.flatMap { context => + val request = s"${context.request.method.name()} ${context.request.uri.path}" + + extractClientIP.flatMap { maybeRemoteAddr => + val remoteAddr = maybeRemoteAddr.toIP.getOrElse("-") + + optionalHeaderValueByType(Referer).flatMap { maybeReferer => + val referer = maybeReferer.map(_.uri).getOrElse("-") + + optionalHeaderValueByType(`User-Agent`).flatMap { maybeUserAgent => + val userAgent = maybeUserAgent.map(_.products.map(_.toString()).mkString(" ")).getOrElse("-") + + optionalHeaderValueByType(Authorization).flatMap { maybeAuth => + val remoteUser = maybeAuth + .map(_.credentials) + .collect { case BasicHttpCredentials(username, _) => + username + } + .getOrElse("-") + + val start = Instant.now.toEpochMilli + val timeLocal = HttpLog.httpLogDatetimeFormatter.format(Instant.now) + + mapResponse { response => + val time = (Instant.now.toEpochMilli - start) / 1000.0 + + val status = response.status.intValue() + val bytesSent = response.entity.getContentLengthOption.getAsLong + + httpLogger.debug( + s"""$remoteAddr - $remoteUser [$timeLocal] "$request" $status $bytesSent "$referer" "$userAgent" $time""" + ) + response + } + } + } + } + } + } + +} + +object HttpLog { + + val httpLogDatetimeFormatter: DateTimeFormatter = + DateTimeFormatter + .ofPattern("dd/MMM/uuuu:HH:mm:ss xx ") + .withLocale(Locale.UK) + .withZone(ZoneId.systemDefault()) +} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/RootEndpoint.scala b/src/main/scala/me/arcanis/ffxivbis/http/RootEndpoint.scala index fdc0b76..a81d910 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/RootEndpoint.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/RootEndpoint.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -8,44 +8,30 @@ */ package me.arcanis.ffxivbis.http -import java.time.Instant - import akka.actor.typed.{ActorRef, ActorSystem, Scheduler} import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server._ import akka.util.Timeout -import com.typesafe.scalalogging.{Logger, StrictLogging} +import com.typesafe.scalalogging.StrictLogging import me.arcanis.ffxivbis.http.api.v1.RootApiV1Endpoint import me.arcanis.ffxivbis.http.view.RootView import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} class RootEndpoint(system: ActorSystem[Nothing], storage: ActorRef[Message], provider: ActorRef[BiSProviderMessage]) - extends StrictLogging { + extends StrictLogging + with HttpLog { import me.arcanis.ffxivbis.utils.Implicits._ private val config = system.settings.config implicit val scheduler: Scheduler = system.scheduler - implicit val timeout: Timeout = - config.getDuration("me.arcanis.ffxivbis.settings.request-timeout") + implicit val timeout: Timeout = config.getDuration("me.arcanis.ffxivbis.settings.request-timeout") - private val rootApiV1Endpoint: RootApiV1Endpoint = new RootApiV1Endpoint(storage, provider, config) - private val rootView: RootView = new RootView(storage, provider) - private val swagger: Swagger = new Swagger(config) - private val httpLogger = Logger("http") + private val auth = AuthorizationProvider(config, storage, timeout, scheduler) - private val withHttpLog: Directive0 = - extractRequestContext.flatMap { context => - val start = Instant.now.toEpochMilli - mapResponse { response => - val time = (Instant.now.toEpochMilli - start) / 1000.0 - httpLogger.debug( - s"""- - [${Instant.now}] "${context.request.method.name()} ${context.request.uri.path}" ${response.status - .intValue()} ${response.entity.getContentLengthOption.getAsLong} $time""" - ) - response - } - } + private val rootApiV1Endpoint = new RootApiV1Endpoint(storage, auth, provider, config) + private val rootView = new RootView(auth) + private val swagger = new Swagger(config) def route: Route = withHttpLog { @@ -68,7 +54,7 @@ class RootEndpoint(system: ActorSystem[Nothing], storage: ActorRef[Message], pro } ~ rootView.route private def swaggerUIRoute: Route = - path("swagger") { - getFromResource("html/swagger.html") + path("api-docs") { + getFromResource("html/redoc.html") } } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/Swagger.scala b/src/main/scala/me/arcanis/ffxivbis/http/Swagger.scala index 0c6dc4c..c4da73e 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/Swagger.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/Swagger.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). 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 723c3c1..24f7b28 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -21,14 +21,19 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.{Operation, Parameter} import jakarta.ws.rs._ import me.arcanis.ffxivbis.http.api.v1.json._ -import me.arcanis.ffxivbis.http.{Authorization, BiSHelper} +import me.arcanis.ffxivbis.http.helpers.BiSHelper +import me.arcanis.ffxivbis.http.{Authorization, AuthorizationProvider} import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} import me.arcanis.ffxivbis.models.PlayerId import scala.util.{Failure, Success} @Path("/api/v1") -class BiSEndpoint(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])(implicit +class BiSEndpoint( + override val storage: ActorRef[Message], + override val provider: ActorRef[BiSProviderMessage], + override val auth: AuthorizationProvider +)(implicit timeout: Timeout, scheduler: Scheduler ) extends BiSHelper @@ -49,29 +54,29 @@ class BiSEndpoint(override val storage: ActorRef[Message], override val provider requestBody = new RequestBody( description = "player best in slot description", required = true, - content = Array(new Content(schema = new Schema(implementation = classOf[PlayerBiSLinkResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[PlayerBiSLinkModel]))) ), responses = Array( new ApiResponse(responseCode = "201", description = "Best in slot set has been created"), new ApiResponse( responseCode = "400", description = "Invalid parameters were supplied", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "401", description = "Supplied authorization is invalid", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "403", description = "Access is forbidden", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))), @@ -82,11 +87,10 @@ class BiSEndpoint(override val storage: ActorRef[Message], override val provider extractExecutionContext { implicit executionContext => authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => put { - entity(as[PlayerBiSLinkResponse]) { bisLink => + entity(as[PlayerBiSLinkModel]) { bisLink => val playerId = bisLink.playerId.withPartyId(partyId) - onComplete(putBiS(playerId, bisLink.link)) { - case Success(_) => complete(StatusCodes.Created, HttpEntity.Empty) - case Failure(exception) => throw exception + onSuccess(putBiS(playerId, bisLink.link)) { + complete(StatusCodes.Created, HttpEntity.Empty) } } } @@ -116,24 +120,24 @@ class BiSEndpoint(override val storage: ActorRef[Message], override val provider description = "Best in slot", content = Array( new Content( - array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse])) + array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerModel])) ) ) ), new ApiResponse( responseCode = "401", description = "Supplied authorization is invalid", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "403", description = "Access is forbidden", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))), @@ -146,9 +150,8 @@ class BiSEndpoint(override val storage: ActorRef[Message], override val provider get { parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) => val playerId = PlayerId(partyId, maybeNick, maybeJob) - onComplete(bis(partyId, playerId)) { - case Success(response) => complete(response.map(PlayerResponse.fromPlayer)) - case Failure(exception) => throw exception + onSuccess(bis(partyId, playerId)) { response => + complete(response.map(PlayerModel.fromPlayer)) } } } @@ -169,29 +172,29 @@ class BiSEndpoint(override val storage: ActorRef[Message], override val provider requestBody = new RequestBody( description = "action and piece description", required = true, - content = Array(new Content(schema = new Schema(implementation = classOf[PieceActionResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[PieceActionModel]))) ), responses = Array( new ApiResponse(responseCode = "202", description = "Best in slot set has been modified"), new ApiResponse( responseCode = "400", description = "Invalid parameters were supplied", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "401", description = "Supplied authorization is invalid", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "403", description = "Access is forbidden", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))), @@ -202,11 +205,10 @@ class BiSEndpoint(override val storage: ActorRef[Message], override val provider extractExecutionContext { implicit executionContext => authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => post { - entity(as[PieceActionResponse]) { action => + entity(as[PieceActionModel]) { action => val playerId = action.playerId.withPartyId(partyId) - onComplete(doModifyBiS(action.action, playerId, action.piece.toPiece)) { - case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty) - case Failure(exception) => throw exception + onSuccess(doModifyBiS(action.action, playerId, action.piece.toPiece)) { + complete(StatusCodes.Accepted, HttpEntity.Empty) } } } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/HttpHandler.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/HttpHandler.scala index 6a85c11..dc4b2b1 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/HttpHandler.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/HttpHandler.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -19,18 +19,18 @@ trait HttpHandler extends StrictLogging { this: JsonSupport => def exceptionHandler: ExceptionHandler = ExceptionHandler { case ex: IllegalArgumentException => - complete(StatusCodes.BadRequest, ErrorResponse(ex.getMessage)) + complete(StatusCodes.BadRequest, ErrorModel(ex.getMessage)) case other: Exception => logger.error("exception during request completion", other) - complete(StatusCodes.InternalServerError, ErrorResponse("unknown server error")) + complete(StatusCodes.InternalServerError, ErrorModel("unknown server error")) } def rejectionHandler: RejectionHandler = RejectionHandler.default .mapRejectionResponse { case response @ HttpResponse(_, _, entity: HttpEntity.Strict, _) => - val message = ErrorResponse(entity.data.utf8String).toJson + val message = ErrorModel(entity.data.utf8String).toJson response.withEntity(HttpEntity(ContentTypes.`application/json`, message.compactPrint)) case other => other } 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 f5f695a..62a7c03 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -21,20 +21,23 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.{Operation, Parameter} import jakarta.ws.rs._ import me.arcanis.ffxivbis.http.api.v1.json._ -import me.arcanis.ffxivbis.http.{Authorization, LootHelper} +import me.arcanis.ffxivbis.http.helpers.LootHelper +import me.arcanis.ffxivbis.http.{Authorization, AuthorizationProvider} import me.arcanis.ffxivbis.messages.Message import me.arcanis.ffxivbis.models.PlayerId import scala.util.{Failure, Success} @Path("/api/v1") -class LootEndpoint(override val storage: ActorRef[Message])(implicit timeout: Timeout, scheduler: Scheduler) - extends LootHelper +class LootEndpoint(override val storage: ActorRef[Message], override val auth: AuthorizationProvider)(implicit + timeout: Timeout, + scheduler: Scheduler +) extends LootHelper with Authorization with JsonSupport with HttpHandler { - def route: Route = getLoot ~ modifyLoot + def route: Route = getLoot ~ modifyLoot ~ suggestLoot @GET @Path("party/{partyId}/loot") @@ -58,24 +61,24 @@ class LootEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti description = "Loot list", content = Array( new Content( - array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse])) + array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerModel])) ) ) ), new ApiResponse( responseCode = "401", description = "Supplied authorization is invalid", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "403", description = "Access is forbidden", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))), @@ -88,9 +91,8 @@ class LootEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti get { parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) => val playerId = PlayerId(partyId, maybeNick, maybeJob) - onComplete(loot(partyId, playerId)) { - case Success(response) => complete(response.map(PlayerResponse.fromPlayer)) - case Failure(exception) => throw exception + onSuccess(loot(partyId, playerId)) { response => + complete(response.map(PlayerModel.fromPlayer)) } } } @@ -110,29 +112,29 @@ class LootEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti requestBody = new RequestBody( description = "action and piece description", required = true, - content = Array(new Content(schema = new Schema(implementation = classOf[PieceActionResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[PieceActionModel]))) ), responses = Array( new ApiResponse(responseCode = "202", description = "Loot list has been modified"), new ApiResponse( responseCode = "400", description = "Invalid parameters were supplied", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "401", description = "Supplied authorization is invalid", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "403", description = "Access is forbidden", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))), @@ -143,11 +145,10 @@ class LootEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti extractExecutionContext { implicit executionContext => authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => post { - entity(as[PieceActionResponse]) { action => + entity(as[PieceActionModel]) { action => val playerId = action.playerId.withPartyId(partyId) - onComplete(doModifyLoot(action.action, playerId, action.piece.toPiece, action.isFreeLoot)) { - case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty) - case Failure(exception) => throw exception + onSuccess(doModifyLoot(action.action, playerId, action.piece.toPiece, action.isFreeLoot)) { + complete(StatusCodes.Accepted, HttpEntity.Empty) } } } @@ -168,7 +169,7 @@ class LootEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti requestBody = new RequestBody( description = "piece description", required = true, - content = Array(new Content(schema = new Schema(implementation = classOf[PieceResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[PieceModel]))) ), responses = Array( new ApiResponse( @@ -176,29 +177,29 @@ class LootEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti description = "Players with counters ordered by priority to get this item", content = Array( new Content( - array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerIdWithCountersResponse])), + array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerIdWithCountersModel])), ) ) ), new ApiResponse( responseCode = "400", description = "Invalid parameters were supplied", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "401", description = "Supplied authorization is invalid", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "403", description = "Access is forbidden", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))), @@ -209,10 +210,9 @@ class LootEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti extractExecutionContext { implicit executionContext => authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => put { - entity(as[PieceResponse]) { piece => - onComplete(suggestPiece(partyId, piece.toPiece)) { - case Success(response) => complete(response.map(PlayerIdWithCountersResponse.fromPlayerId)) - case Failure(exception) => throw exception + entity(as[PieceModel]) { piece => + onSuccess(suggestPiece(partyId, piece.toPiece)) { response => + complete(response.map(PlayerIdWithCountersModel.fromPlayerId)) } } } 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 3174882..2ce7dff 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -21,14 +21,18 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.{Operation, Parameter} import jakarta.ws.rs._ import me.arcanis.ffxivbis.http.api.v1.json._ -import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper} +import me.arcanis.ffxivbis.http.helpers.PlayerHelper +import me.arcanis.ffxivbis.http.{Authorization, AuthorizationProvider} import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} import scala.util.{Failure, Success} @Path("/api/v1") -class PartyEndpoint(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])( - implicit +class PartyEndpoint( + override val storage: ActorRef[Message], + override val provider: ActorRef[BiSProviderMessage], + override val auth: AuthorizationProvider +)(implicit timeout: Timeout, scheduler: Scheduler ) extends PlayerHelper @@ -51,22 +55,22 @@ class PartyEndpoint(override val storage: ActorRef[Message], override val provid new ApiResponse( responseCode = "200", description = "Party description", - content = Array(new Content(schema = new Schema(implementation = classOf[PartyDescriptionResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[PartyDescriptionModel]))) ), new ApiResponse( responseCode = "401", description = "Supplied authorization is invalid", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "403", description = "Access is forbidden", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))), @@ -77,9 +81,8 @@ class PartyEndpoint(override val storage: ActorRef[Message], override val provid extractExecutionContext { implicit executionContext => authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => get { - onComplete(getPartyDescription(partyId)) { - case Success(response) => complete(PartyDescriptionResponse.fromDescription(response)) - case Failure(exception) => throw exception + onSuccess(getPartyDescription(partyId)) { response => + complete(PartyDescriptionModel.fromDescription(response)) } } } @@ -98,29 +101,29 @@ class PartyEndpoint(override val storage: ActorRef[Message], override val provid requestBody = new RequestBody( description = "new party description", required = true, - content = Array(new Content(schema = new Schema(implementation = classOf[PartyDescriptionResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[PartyDescriptionModel]))) ), responses = Array( new ApiResponse(responseCode = "202", description = "Party description has been modified"), new ApiResponse( responseCode = "400", description = "Invalid parameters were supplied", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "401", description = "Supplied authorization is invalid", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "403", description = "Access is forbidden", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))), @@ -131,11 +134,10 @@ class PartyEndpoint(override val storage: ActorRef[Message], override val provid extractExecutionContext { implicit executionContext => authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => post { - entity(as[PartyDescriptionResponse]) { partyDescription => + entity(as[PartyDescriptionModel]) { partyDescription => val description = partyDescription.copy(partyId = partyId) - onComplete(updateDescription(description.toDescription)) { - case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty) - case Failure(exception) => throw exception + onSuccess(updateDescription(description.toDescription)) { + complete(StatusCodes.Accepted, HttpEntity.Empty) } } } 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 5b1d930..5f0e449 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -21,15 +21,19 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.{Operation, Parameter} import jakarta.ws.rs._ import me.arcanis.ffxivbis.http.api.v1.json._ -import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper} +import me.arcanis.ffxivbis.http.helpers.PlayerHelper +import me.arcanis.ffxivbis.http.{Authorization, AuthorizationProvider} import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} import me.arcanis.ffxivbis.models.PlayerId import scala.util.{Failure, Success} @Path("/api/v1") -class PlayerEndpoint(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])( - implicit +class PlayerEndpoint( + override val storage: ActorRef[Message], + override val provider: ActorRef[BiSProviderMessage], + override val auth: AuthorizationProvider +)(implicit timeout: Timeout, scheduler: Scheduler ) extends PlayerHelper @@ -37,7 +41,7 @@ class PlayerEndpoint(override val storage: ActorRef[Message], override val provi with JsonSupport with HttpHandler { - def route: Route = getParty ~ modifyParty + def route: Route = getParty ~ getPartyStats ~ modifyParty @GET @Path("party/{partyId}") @@ -61,24 +65,24 @@ class PlayerEndpoint(override val storage: ActorRef[Message], override val provi description = "Players list", content = Array( new Content( - array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse])), + array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerModel])), ) ) ), new ApiResponse( responseCode = "401", description = "Supplied authorization is invalid", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "403", description = "Access is forbidden", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))), @@ -91,9 +95,69 @@ class PlayerEndpoint(override val storage: ActorRef[Message], override val provi get { parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) => val playerId = PlayerId(partyId, maybeNick, maybeJob) - onComplete(getPlayers(partyId, playerId)) { - case Success(response) => complete(response.map(PlayerResponse.fromPlayer)) - case Failure(exception) => throw exception + onSuccess(getPlayers(partyId, playerId)) { response => + complete(response.map(PlayerModel.fromPlayer)) + } + } + } + } + } + } + + @GET + @Path("party/{partyId}/stats") + @Produces(value = Array("application/json")) + @Operation( + 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 = "nick", + in = ParameterIn.QUERY, + description = "player nick name to filter", + example = "Siuan Sanche" + ), + new Parameter(name = "job", in = ParameterIn.QUERY, description = "player job to filter", example = "DNC"), + ), + responses = Array( + new ApiResponse( + responseCode = "200", + description = "Party loot statistics", + content = Array( + new Content( + array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerIdWithCountersModel])), + ) + ) + ), + new ApiResponse( + responseCode = "401", + description = "Supplied authorization is invalid", + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) + ), + new ApiResponse( + responseCode = "403", + description = "Access is forbidden", + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) + ), + new ApiResponse( + responseCode = "500", + description = "Internal server error", + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) + ), + ), + security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))), + tags = Array("party"), + ) + def getPartyStats: Route = + path("party" / Segment / "stats") { partyId => + extractExecutionContext { implicit executionContext => + authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => + get { + parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) => + val playerId = PlayerId(partyId, maybeNick, maybeJob) + onSuccess(getPlayers(partyId, playerId)) { response => + complete(response.map(player => PlayerIdWithCountersModel.fromPlayerId(player.withCounters(None)))) } } } @@ -113,29 +177,29 @@ class PlayerEndpoint(override val storage: ActorRef[Message], override val provi requestBody = new RequestBody( description = "player description", required = true, - content = Array(new Content(schema = new Schema(implementation = classOf[PlayerActionResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[PlayerActionModel]))) ), responses = Array( new ApiResponse(responseCode = "202", description = "Party has been modified"), new ApiResponse( responseCode = "400", description = "Invalid parameters were supplied", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "401", description = "Supplied authorization is invalid", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "403", description = "Access is forbidden", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))), @@ -145,11 +209,12 @@ class PlayerEndpoint(override val storage: ActorRef[Message], override val provi path("party" / Segment) { partyId => extractExecutionContext { implicit executionContext => authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => - entity(as[PlayerActionResponse]) { action => - val player = action.playerId.toPlayer.copy(partyId = partyId) - onComplete(doModifyPlayer(action.action, player)) { - case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty) - case Failure(exception) => throw exception + post { + entity(as[PlayerActionModel]) { action => + val player = action.playerId.toPlayer.copy(partyId = partyId) + onSuccess(doModifyPlayer(action.action, player)) { + complete(StatusCodes.Accepted, HttpEntity.Empty) + } } } } 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 a33c547..0fecf00 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -13,21 +13,27 @@ 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.AuthorizationProvider import me.arcanis.ffxivbis.http.api.v1.json.JsonSupport import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} -class RootApiV1Endpoint(storage: ActorRef[Message], provider: ActorRef[BiSProviderMessage], config: Config)(implicit +class RootApiV1Endpoint( + storage: ActorRef[Message], + auth: AuthorizationProvider, + provider: ActorRef[BiSProviderMessage], + config: Config +)(implicit timeout: Timeout, scheduler: Scheduler ) extends JsonSupport with HttpHandler { - private val biSEndpoint = new BiSEndpoint(storage, provider) - private val lootEndpoint = new LootEndpoint(storage) - private val partyEndpoint = new PartyEndpoint(storage, provider) - private val playerEndpoint = new PlayerEndpoint(storage, provider) + private val biSEndpoint = new BiSEndpoint(storage, provider, auth) + private val lootEndpoint = new LootEndpoint(storage, auth) + private val partyEndpoint = new PartyEndpoint(storage, provider, auth) + private val playerEndpoint = new PlayerEndpoint(storage, provider, auth) private val typesEndpoint = new TypesEndpoint(config) - private val userEndpoint = new UserEndpoint(storage) + private val userEndpoint = new UserEndpoint(storage, auth) def route: Route = handleExceptions(exceptionHandler) { 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 1df51c2..a1dc408 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -11,17 +11,48 @@ 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.Operation 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 jakarta.ws.rs._ import me.arcanis.ffxivbis.http.api.v1.json._ -import me.arcanis.ffxivbis.models.{Job, Party, Permission, Piece, PieceType} +import me.arcanis.ffxivbis.models._ @Path("/api/v1") class TypesEndpoint(config: Config) extends JsonSupport { - def route: Route = getJobs ~ getPermissions ~ getPieces ~ getPieceTypes ~ getPriority + def route: Route = getAllJobs ~ getJobs ~ getPermissions ~ getPieces ~ getPieceTypes ~ getPriority + + @GET + @Path("types/jobs/all") + @Produces(value = Array("application/json")) + @Operation( + summary = "full jobs list", + description = "Returns the available jobs including any job", + responses = Array( + new ApiResponse( + responseCode = "200", + description = "List of available jobs with AnyJob", + 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[ErrorModel]))) + ), + ), + tags = Array("types"), + ) + def getAllJobs: Route = + path("types" / "jobs" / "all") { + get { + complete(Job.availableWithAnyJob.map(_.toString)) + } + } @GET @Path("types/jobs") @@ -42,7 +73,7 @@ class TypesEndpoint(config: Config) extends JsonSupport { new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), tags = Array("types"), @@ -50,7 +81,7 @@ class TypesEndpoint(config: Config) extends JsonSupport { def getJobs: Route = path("types" / "jobs") { get { - complete(Job.availableWithAnyJob.map(_.toString)) + complete(Job.available.map(_.toString)) } } @@ -73,7 +104,7 @@ class TypesEndpoint(config: Config) extends JsonSupport { new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), tags = Array("types"), @@ -104,7 +135,7 @@ class TypesEndpoint(config: Config) extends JsonSupport { new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), tags = Array("types"), @@ -135,7 +166,7 @@ class TypesEndpoint(config: Config) extends JsonSupport { new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), tags = Array("types"), @@ -166,7 +197,7 @@ class TypesEndpoint(config: Config) extends JsonSupport { new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), tags = Array("types"), 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 9726566..e2d9232 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -21,19 +21,22 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.{Operation, Parameter} import jakarta.ws.rs._ import me.arcanis.ffxivbis.http.api.v1.json._ -import me.arcanis.ffxivbis.http.{Authorization, UserHelper} +import me.arcanis.ffxivbis.http.helpers.UserHelper +import me.arcanis.ffxivbis.http.{Authorization, AuthorizationProvider} import me.arcanis.ffxivbis.messages.Message import me.arcanis.ffxivbis.models.Permission import scala.util.{Failure, Success} @Path("/api/v1") -class UserEndpoint(override val storage: ActorRef[Message])(implicit timeout: Timeout, scheduler: Scheduler) - extends UserHelper +class UserEndpoint(override val storage: ActorRef[Message], override val auth: AuthorizationProvider)(implicit + timeout: Timeout, + scheduler: Scheduler +) extends UserHelper with Authorization with JsonSupport { - def route: Route = createParty ~ createUser ~ deleteUser ~ getUsers + def route: Route = createParty ~ createUser ~ deleteUser ~ getUsers ~ getUsersCurrent @PUT @Path("party") @@ -44,24 +47,28 @@ class UserEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti requestBody = new RequestBody( description = "party administrator description", required = true, - content = Array(new Content(schema = new Schema(implementation = classOf[UserResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[UserModel]))) ), responses = Array( - new ApiResponse(responseCode = "200", description = "Party has been created"), + new ApiResponse( + responseCode = "200", + description = "Party has been created", + content = Array(new Content(schema = new Schema(implementation = classOf[PartyIdModel]))) + ), new ApiResponse( responseCode = "400", description = "Invalid parameters were supplied", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "406", description = "Party with the specified ID already exists", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), tags = Array("party"), @@ -70,15 +77,12 @@ class UserEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti path("party") { extractExecutionContext { implicit executionContext => put { - entity(as[UserResponse]) { user => - onComplete(newPartyId) { - case Success(partyId) => - val admin = user.toUser.copy(partyId = partyId, permission = Permission.admin) - onComplete(addUser(admin, isHashedPassword = false)) { - case Success(_) => complete(PartyIdResponse(partyId)) - case Failure(exception) => throw exception - } - case Failure(exception) => throw exception + entity(as[UserModel]) { user => + onSuccess(newPartyId) { partyId => + val admin = user.toUser.copy(partyId = partyId, permission = Permission.admin) + onSuccess(addUser(admin, isHashedPassword = false)) { + complete(PartyIdModel(partyId)) + } } } } @@ -97,29 +101,29 @@ class UserEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti requestBody = new RequestBody( description = "user description", required = true, - content = Array(new Content(schema = new Schema(implementation = classOf[UserResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[UserModel]))) ), responses = Array( new ApiResponse(responseCode = "201", description = "User has been created"), new ApiResponse( responseCode = "400", description = "Invalid parameters were supplied", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "401", description = "Supplied authorization is invalid", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "403", description = "Access is forbidden", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))), @@ -130,11 +134,10 @@ class UserEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti extractExecutionContext { implicit executionContext => authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ => post { - entity(as[UserResponse]) { user => + entity(as[UserModel]) { user => val withPartyId = user.toUser.copy(partyId = partyId) - onComplete(addUser(withPartyId, isHashedPassword = false)) { - case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty) - case Failure(exception) => throw exception + onSuccess(addUser(withPartyId, isHashedPassword = false)) { + complete(StatusCodes.Accepted, HttpEntity.Empty) } } } @@ -156,17 +159,17 @@ class UserEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti new ApiResponse( responseCode = "401", description = "Supplied authorization is invalid", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "403", description = "Access is forbidden", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))), @@ -177,9 +180,8 @@ class UserEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti extractExecutionContext { implicit executionContext => authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ => delete { - onComplete(removeUser(partyId, username)) { - case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty) - case Failure(exception) => throw exception + onSuccess(removeUser(partyId, username)) { + complete(StatusCodes.Accepted, HttpEntity.Empty) } } } @@ -201,27 +203,27 @@ class UserEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti description = "Users list", content = Array( new Content( - array = new ArraySchema(schema = new Schema(implementation = classOf[UserResponse])), + array = new ArraySchema(schema = new Schema(implementation = classOf[UserModel])), ) ) ), new ApiResponse( responseCode = "401", description = "Supplied authorization is invalid", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "403", description = "Access is forbidden", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), - security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))), + security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))), tags = Array("users"), ) def getUsers: Route = @@ -229,12 +231,56 @@ class UserEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti extractExecutionContext { implicit executionContext => authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ => get { - onComplete(users(partyId)) { - case Success(response) => complete(response.map(UserResponse.fromUser)) - case Failure(exception) => throw exception + onSuccess(users(partyId)) { response => + complete(response.map(UserModel.fromUser)) } } } } } + + @GET + @Path("party/{partyId}/users/current") + @Produces(value = Array("application/json")) + @Operation( + 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"), + ), + responses = Array( + new ApiResponse( + responseCode = "200", + description = "User descriptor", + content = Array(new Content(schema = new Schema(implementation = classOf[UserModel]))) + ), + new ApiResponse( + responseCode = "401", + description = "Supplied authorization is invalid", + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) + ), + new ApiResponse( + responseCode = "403", + description = "Access is forbidden", + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) + ), + new ApiResponse( + responseCode = "500", + description = "Internal server error", + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) + ), + ), + security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))), + tags = Array("users"), + ) + def getUsersCurrent: Route = + path("party" / Segment / "users" / "current") { partyId => + extractExecutionContext { implicit executionContext => + authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { user => + get { + complete(UserModel.fromUser(user)) + } + } + } + } } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/ApiAction.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/ApiAction.scala index 3a8d3af..6051720 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/ApiAction.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/ApiAction.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyIdResponse.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/ErrorModel.scala similarity index 65% rename from src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyIdResponse.scala rename to src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/ErrorModel.scala index 81bfcff..8de54cb 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyIdResponse.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/ErrorModel.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -10,4 +10,4 @@ package me.arcanis.ffxivbis.http.api.v1.json import io.swagger.v3.oas.annotations.media.Schema -case class PartyIdResponse(@Schema(description = "party id", required = true) partyId: String) +case class ErrorModel(@Schema(description = "error message", required = true) message: String) diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/JsonSupport.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/JsonSupport.scala index 4486244..85e6bbc 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/JsonSupport.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/JsonSupport.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -8,12 +8,12 @@ */ package me.arcanis.ffxivbis.http.api.v1.json -import java.time.Instant - import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport import me.arcanis.ffxivbis.models.Permission import spray.json._ +import java.time.Instant + trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol { private def enumFormat[E <: Enumeration](enum: E): RootJsonFormat[E#Value] = @@ -38,19 +38,19 @@ trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol { implicit val actionFormat: RootJsonFormat[ApiAction.Value] = enumFormat(ApiAction) implicit val permissionFormat: RootJsonFormat[Permission.Value] = enumFormat(Permission) - implicit val errorFormat: RootJsonFormat[ErrorResponse] = jsonFormat1(ErrorResponse.apply) - implicit val partyIdFormat: RootJsonFormat[PartyIdResponse] = jsonFormat1(PartyIdResponse.apply) - implicit val pieceFormat: RootJsonFormat[PieceResponse] = jsonFormat3(PieceResponse.apply) - implicit val lootFormat: RootJsonFormat[LootResponse] = jsonFormat3(LootResponse.apply) - implicit val partyDescriptionFormat: RootJsonFormat[PartyDescriptionResponse] = jsonFormat2( - PartyDescriptionResponse.apply + implicit val errorFormat: RootJsonFormat[ErrorModel] = jsonFormat1(ErrorModel.apply) + implicit val partyIdFormat: RootJsonFormat[PartyIdModel] = jsonFormat1(PartyIdModel.apply) + implicit val pieceFormat: RootJsonFormat[PieceModel] = jsonFormat3(PieceModel.apply) + implicit val lootFormat: RootJsonFormat[LootModel] = jsonFormat3(LootModel.apply) + implicit val partyDescriptionFormat: RootJsonFormat[PartyDescriptionModel] = jsonFormat2( + PartyDescriptionModel.apply ) - implicit val playerFormat: RootJsonFormat[PlayerResponse] = jsonFormat7(PlayerResponse.apply) - implicit val playerActionFormat: RootJsonFormat[PlayerActionResponse] = jsonFormat2(PlayerActionResponse.apply) - implicit val playerIdFormat: RootJsonFormat[PlayerIdResponse] = jsonFormat3(PlayerIdResponse.apply) - implicit val pieceActionFormat: RootJsonFormat[PieceActionResponse] = jsonFormat4(PieceActionResponse.apply) - implicit val playerBiSLinkFormat: RootJsonFormat[PlayerBiSLinkResponse] = jsonFormat2(PlayerBiSLinkResponse.apply) - implicit val playerIdWithCountersFormat: RootJsonFormat[PlayerIdWithCountersResponse] = - jsonFormat9(PlayerIdWithCountersResponse.apply) - implicit val userFormat: RootJsonFormat[UserResponse] = jsonFormat4(UserResponse.apply) + implicit val playerFormat: RootJsonFormat[PlayerModel] = jsonFormat9(PlayerModel.apply) + implicit val playerActionFormat: RootJsonFormat[PlayerActionModel] = jsonFormat2(PlayerActionModel.apply) + implicit val playerIdFormat: RootJsonFormat[PlayerIdModel] = jsonFormat3(PlayerIdModel.apply) + implicit val pieceActionFormat: RootJsonFormat[PieceActionModel] = jsonFormat4(PieceActionModel.apply) + implicit val playerBiSLinkFormat: RootJsonFormat[PlayerBiSLinkModel] = jsonFormat2(PlayerBiSLinkModel.apply) + implicit val playerIdWithCountersFormat: RootJsonFormat[PlayerIdWithCountersModel] = + jsonFormat9(PlayerIdWithCountersModel.apply) + implicit val userFormat: RootJsonFormat[UserModel] = jsonFormat4(UserModel.apply) } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/LootResponse.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/LootModel.scala similarity index 54% rename from src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/LootResponse.scala rename to src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/LootModel.scala index 0bf0d42..8f72c85 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/LootResponse.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/LootModel.scala @@ -1,12 +1,20 @@ +/* + * Copyright (c) 2019-2022 Evgeniy Alekseev. + * + * This file is part of ffxivbis + * (see https://github.com/arcan1s/ffxivbis). + * + * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause + */ package me.arcanis.ffxivbis.http.api.v1.json -import java.time.Instant - import io.swagger.v3.oas.annotations.media.Schema import me.arcanis.ffxivbis.models.Loot -case class LootResponse( - @Schema(description = "looted piece", required = true) piece: PieceResponse, +import java.time.Instant + +case class LootModel( + @Schema(description = "looted piece", required = true) piece: PieceModel, @Schema(description = "loot timestamp", required = true) timestamp: Instant, @Schema(description = "is loot free for all", required = true) isFreeLoot: Boolean ) { @@ -14,8 +22,8 @@ case class LootResponse( def toLoot: Loot = Loot(-1, piece.toPiece, timestamp, isFreeLoot) } -object LootResponse { +object LootModel { - def fromLoot(loot: Loot): LootResponse = - LootResponse(PieceResponse.fromPiece(loot.piece), loot.timestamp, loot.isFreeLoot) + def fromLoot(loot: Loot): LootModel = + LootModel(PieceModel.fromPiece(loot.piece), loot.timestamp, loot.isFreeLoot) } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyDescriptionResponse.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyDescriptionModel.scala similarity index 65% rename from src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyDescriptionResponse.scala rename to src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyDescriptionModel.scala index 7111c14..bfb5b2b 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyDescriptionResponse.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyDescriptionModel.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -11,16 +11,16 @@ package me.arcanis.ffxivbis.http.api.v1.json import io.swagger.v3.oas.annotations.media.Schema import me.arcanis.ffxivbis.models.PartyDescription -case class PartyDescriptionResponse( - @Schema(description = "party id", required = true) partyId: String, +case class PartyDescriptionModel( + @Schema(description = "party id", required = true, example = "abcdefgh") partyId: String, @Schema(description = "party name") partyAlias: Option[String] ) { def toDescription: PartyDescription = PartyDescription(partyId, partyAlias) } -object PartyDescriptionResponse { +object PartyDescriptionModel { - def fromDescription(description: PartyDescription): PartyDescriptionResponse = - PartyDescriptionResponse(description.partyId, description.partyAlias) + def fromDescription(description: PartyDescription): PartyDescriptionModel = + PartyDescriptionModel(description.partyId, description.partyAlias) } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/ErrorResponse.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyIdModel.scala similarity index 62% rename from src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/ErrorResponse.scala rename to src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyIdModel.scala index ba3e7a0..88fc028 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/ErrorResponse.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyIdModel.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -10,4 +10,4 @@ package me.arcanis.ffxivbis.http.api.v1.json import io.swagger.v3.oas.annotations.media.Schema -case class ErrorResponse(@Schema(description = "error message", required = true) message: String) +case class PartyIdModel(@Schema(description = "party id", required = true, example = "abcdefgh") partyId: String) diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PieceActionResponse.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PieceActionModel.scala similarity index 71% rename from src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PieceActionResponse.scala rename to src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PieceActionModel.scala index b90026c..0487f22 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PieceActionResponse.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PieceActionModel.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -10,14 +10,14 @@ package me.arcanis.ffxivbis.http.api.v1.json import io.swagger.v3.oas.annotations.media.Schema -case class PieceActionResponse( +case class PieceActionModel( @Schema( description = "action to perform", required = true, `type` = "string", allowableValues = Array("add", "remove") ) action: ApiAction.Value, - @Schema(description = "piece description", required = true) piece: PieceResponse, - @Schema(description = "player description", required = true) playerId: PlayerIdResponse, - @Schema(description = "is piece free to roll or not") isFreeLoot: Option[Boolean] + @Schema(description = "piece description", required = true) piece: PieceModel, + @Schema(description = "player description", required = true) playerId: PlayerIdModel, + @Schema(description = "is piece free to roll or not", `type` = "boolean") isFreeLoot: Option[Boolean] ) diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PieceResponse.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PieceModel.scala similarity index 67% rename from src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PieceResponse.scala rename to src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PieceModel.scala index 9a7e172..b67cb5a 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PieceResponse.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PieceModel.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -11,8 +11,8 @@ package me.arcanis.ffxivbis.http.api.v1.json import io.swagger.v3.oas.annotations.media.Schema import me.arcanis.ffxivbis.models.{Job, Piece, PieceType} -case class PieceResponse( - @Schema(description = "piece type", required = true) pieceType: String, +case class PieceModel( + @Schema(description = "piece type", required = true, example = "Savage") pieceType: String, @Schema(description = "job name to which piece belong or AnyJob", required = true, example = "DNC") job: String, @Schema(description = "piece name", required = true, example = "body") piece: String ) { @@ -20,8 +20,8 @@ case class PieceResponse( def toPiece: Piece = Piece(piece, PieceType.withName(pieceType), Job.withName(job)) } -object PieceResponse { +object PieceModel { - def fromPiece(piece: Piece): PieceResponse = - PieceResponse(piece.pieceType.toString, piece.job.toString, piece.piece) + def fromPiece(piece: Piece): PieceModel = + PieceModel(piece.pieceType.toString, piece.job.toString, piece.piece) } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerActionResponse.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerActionModel.scala similarity index 84% rename from src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerActionResponse.scala rename to src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerActionModel.scala index f79ca93..7b240ff 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerActionResponse.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerActionModel.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -10,7 +10,7 @@ package me.arcanis.ffxivbis.http.api.v1.json import io.swagger.v3.oas.annotations.media.Schema -case class PlayerActionResponse( +case class PlayerActionModel( @Schema( description = "action to perform", required = true, @@ -18,5 +18,5 @@ case class PlayerActionResponse( allowableValues = Array("add", "remove"), example = "add" ) action: ApiAction.Value, - @Schema(description = "player description", required = true) playerId: PlayerResponse + @Schema(description = "player description", required = true) playerId: PlayerModel ) diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerBiSLinkResponse.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerBiSLinkModel.scala similarity index 82% rename from src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerBiSLinkResponse.scala rename to src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerBiSLinkModel.scala index b4625ff..7dfc244 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerBiSLinkResponse.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerBiSLinkModel.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -10,11 +10,11 @@ package me.arcanis.ffxivbis.http.api.v1.json import io.swagger.v3.oas.annotations.media.Schema -case class PlayerBiSLinkResponse( +case class PlayerBiSLinkModel( @Schema( description = "link to player best in slot", required = true, example = "https://ffxiv.ariyala.com/19V5R" ) link: String, - @Schema(description = "player description", required = true) playerId: PlayerIdResponse + @Schema(description = "player description", required = true) playerId: PlayerIdModel ) diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdResponse.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdModel.scala similarity index 75% rename from src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdResponse.scala rename to src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdModel.scala index 2b54925..efca7e6 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdResponse.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdModel.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -11,7 +11,7 @@ package me.arcanis.ffxivbis.http.api.v1.json import io.swagger.v3.oas.annotations.media.Schema import me.arcanis.ffxivbis.models.{Job, PlayerId} -case class PlayerIdResponse( +case class PlayerIdModel( @Schema(description = "unique party ID. Required in responses", example = "abcdefgh") 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 @@ -21,8 +21,8 @@ case class PlayerIdResponse( PlayerId(partyId, Job.withName(job), nick) } -object PlayerIdResponse { +object PlayerIdModel { - def fromPlayerId(playerId: PlayerId): PlayerIdResponse = - PlayerIdResponse(Some(playerId.partyId), playerId.job.toString, playerId.nick) + def fromPlayerId(playerId: PlayerId): PlayerIdModel = + PlayerIdModel(Some(playerId.partyId), playerId.job.toString, playerId.nick) } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdWithCountersResponse.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdWithCountersModel.scala similarity index 84% rename from src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdWithCountersResponse.scala rename to src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdWithCountersModel.scala index 8485102..df72789 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdWithCountersResponse.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdWithCountersModel.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -11,22 +11,22 @@ package me.arcanis.ffxivbis.http.api.v1.json import io.swagger.v3.oas.annotations.media.Schema import me.arcanis.ffxivbis.models.PlayerIdWithCounters -case class PlayerIdWithCountersResponse( +case class PlayerIdWithCountersModel( @Schema(description = "unique party ID", required = true, example = "abcdefgh") 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, @Schema(description = "player loot priority", required = true) priority: Int, @Schema(description = "count of savage pieces in best in slot", required = true) bisCountTotal: Int, - @Schema(description = "count of looted pieces", required = true) lootCount: Int, + @Schema(description = "count of looted pieces of this type", required = true) lootCount: Int, @Schema(description = "count of looted pieces which are parts of best in slot", required = true) lootCountBiS: Int, @Schema(description = "total count of looted pieces", required = true) lootCountTotal: Int ) -object PlayerIdWithCountersResponse { +object PlayerIdWithCountersModel { - def fromPlayerId(playerIdWithCounters: PlayerIdWithCounters): PlayerIdWithCountersResponse = - PlayerIdWithCountersResponse( + def fromPlayerId(playerIdWithCounters: PlayerIdWithCounters): PlayerIdWithCountersModel = + PlayerIdWithCountersModel( playerIdWithCounters.partyId, playerIdWithCounters.job.toString, playerIdWithCounters.nick, diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerResponse.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerModel.scala similarity index 63% rename from src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerResponse.scala rename to src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerModel.scala index 2a4b987..62457fb 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerResponse.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerModel.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -11,14 +11,16 @@ package me.arcanis.ffxivbis.http.api.v1.json import io.swagger.v3.oas.annotations.media.Schema import me.arcanis.ffxivbis.models.{BiS, Job, Player} -case class PlayerResponse( +case class PlayerModel( @Schema(description = "unique party ID", required = true, example = "abcdefgh") 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[PieceResponse]], - @Schema(description = "looted pieces") loot: Option[Seq[LootResponse]], + @Schema(description = "pieces in best in slot") bis: Option[Seq[PieceModel]], + @Schema(description = "looted pieces") loot: Option[Seq[LootModel]], @Schema(description = "link to best in slot", example = "https://ffxiv.ariyala.com/19V5R") link: Option[String], - @Schema(description = "player loot priority", `type` = "number") priority: Option[Int] + @Schema(description = "player loot priority", `type` = "number") priority: Option[Int], + @Schema(description = "count of looted pieces which are parts of best in slot") lootCountBiS: Option[Int], + @Schema(description = "total count of looted pieces", `type` = "number") lootCountTotal: Option[Int], ) { def toPlayer: Player = @@ -34,16 +36,18 @@ case class PlayerResponse( ) } -object PlayerResponse { +object PlayerModel { - def fromPlayer(player: Player): PlayerResponse = - PlayerResponse( + def fromPlayer(player: Player): PlayerModel = + PlayerModel( player.partyId, player.job.toString, player.nick, - Some(player.bis.pieces.map(PieceResponse.fromPiece)), - Some(player.loot.map(LootResponse.fromLoot)), + Some(player.bis.pieces.map(PieceModel.fromPiece)), + Some(player.loot.map(LootModel.fromLoot)), player.link, - Some(player.priority) + Some(player.priority), + Some(player.lootCountBiS), + Some(player.lootCountTotal), ) } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/UserResponse.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/UserModel.scala similarity index 80% rename from src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/UserResponse.scala rename to src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/UserModel.scala index 6786140..437d940 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/UserResponse.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/UserModel.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -11,13 +11,14 @@ package me.arcanis.ffxivbis.http.api.v1.json import io.swagger.v3.oas.annotations.media.Schema import me.arcanis.ffxivbis.models.{Permission, User} -case class UserResponse( +case class UserModel( @Schema(description = "unique party ID", required = true, example = "abcdefgh") 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 = "user permission", defaultValue = "get", + `type` = "string", allowableValues = Array("get", "post", "admin") ) permission: Option[Permission.Value] = None ) { @@ -26,8 +27,8 @@ case class UserResponse( User(partyId, username, password, permission.getOrElse(Permission.get)) } -object UserResponse { +object UserModel { - def fromUser(user: User): UserResponse = - UserResponse(user.partyId, user.username, "", Some(user.permission)) + def fromUser(user: User): UserModel = + UserModel(user.partyId, user.username, "", Some(user.permission)) } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/BiSHelper.scala b/src/main/scala/me/arcanis/ffxivbis/http/helpers/BiSHelper.scala similarity index 82% rename from src/main/scala/me/arcanis/ffxivbis/http/BiSHelper.scala rename to src/main/scala/me/arcanis/ffxivbis/http/helpers/BiSHelper.scala index f1a5fce..5089dbe 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/BiSHelper.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/helpers/BiSHelper.scala @@ -1,18 +1,10 @@ -/* - * Copyright (c) 2019 Evgeniy Alekseev. - * - * This file is part of ffxivbis - * (see https://github.com/arcan1s/ffxivbis). - * - * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause - */ -package me.arcanis.ffxivbis.http +package me.arcanis.ffxivbis.http.helpers import akka.actor.typed.scaladsl.AskPattern.Askable import akka.actor.typed.{ActorRef, Scheduler} import akka.util.Timeout import me.arcanis.ffxivbis.http.api.v1.json.ApiAction -import me.arcanis.ffxivbis.messages.{AddPieceToBis, GetBiS, Message, RemovePieceFromBiS, RemovePiecesFromBiS} +import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId} import scala.concurrent.{ExecutionContext, Future} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/BisProviderHelper.scala b/src/main/scala/me/arcanis/ffxivbis/http/helpers/BisProviderHelper.scala similarity index 67% rename from src/main/scala/me/arcanis/ffxivbis/http/BisProviderHelper.scala rename to src/main/scala/me/arcanis/ffxivbis/http/helpers/BisProviderHelper.scala index 3ddc56e..2a28b0a 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/BisProviderHelper.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/helpers/BisProviderHelper.scala @@ -1,15 +1,7 @@ -/* - * Copyright (c) 2019 Evgeniy Alekseev. - * - * This file is part of ffxivbis - * (see https://github.com/arcan1s/ffxivbis). - * - * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause - */ -package me.arcanis.ffxivbis.http +package me.arcanis.ffxivbis.http.helpers -import akka.actor.typed.{ActorRef, Scheduler} import akka.actor.typed.scaladsl.AskPattern.Askable +import akka.actor.typed.{ActorRef, Scheduler} import akka.util.Timeout import me.arcanis.ffxivbis.messages.{BiSProviderMessage, DownloadBiS} import me.arcanis.ffxivbis.models.{BiS, Job} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/LootHelper.scala b/src/main/scala/me/arcanis/ffxivbis/http/helpers/LootHelper.scala similarity index 83% rename from src/main/scala/me/arcanis/ffxivbis/http/LootHelper.scala rename to src/main/scala/me/arcanis/ffxivbis/http/helpers/LootHelper.scala index 57b1351..ad65a72 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/LootHelper.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/helpers/LootHelper.scala @@ -1,18 +1,10 @@ -/* - * Copyright (c) 2019 Evgeniy Alekseev. - * - * This file is part of ffxivbis - * (see https://github.com/arcan1s/ffxivbis). - * - * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause - */ -package me.arcanis.ffxivbis.http +package me.arcanis.ffxivbis.http.helpers import akka.actor.typed.scaladsl.AskPattern.Askable import akka.actor.typed.{ActorRef, Scheduler} import akka.util.Timeout import me.arcanis.ffxivbis.http.api.v1.json.ApiAction -import me.arcanis.ffxivbis.messages.{AddPieceTo, GetLoot, Message, RemovePieceFrom, SuggestLoot} +import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId, PlayerIdWithCounters} import scala.concurrent.{ExecutionContext, Future} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/PlayerHelper.scala b/src/main/scala/me/arcanis/ffxivbis/http/helpers/PlayerHelper.scala similarity index 85% rename from src/main/scala/me/arcanis/ffxivbis/http/PlayerHelper.scala rename to src/main/scala/me/arcanis/ffxivbis/http/helpers/PlayerHelper.scala index f10e7b2..80a3647 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/PlayerHelper.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/helpers/PlayerHelper.scala @@ -1,18 +1,10 @@ -/* - * Copyright (c) 2019 Evgeniy Alekseev. - * - * This file is part of ffxivbis - * (see https://github.com/arcan1s/ffxivbis). - * - * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause - */ -package me.arcanis.ffxivbis.http +package me.arcanis.ffxivbis.http.helpers -import akka.actor.typed.{ActorRef, Scheduler} import akka.actor.typed.scaladsl.AskPattern.Askable +import akka.actor.typed.{ActorRef, Scheduler} import akka.util.Timeout import me.arcanis.ffxivbis.http.api.v1.json.ApiAction -import me.arcanis.ffxivbis.messages.{AddPieceToBis, AddPlayer, GetParty, GetPartyDescription, GetPlayer, Message, RemovePlayer, UpdateParty} +import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.models.{PartyDescription, Player, PlayerId} import scala.concurrent.{ExecutionContext, Future} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/UserHelper.scala b/src/main/scala/me/arcanis/ffxivbis/http/helpers/UserHelper.scala similarity index 74% rename from src/main/scala/me/arcanis/ffxivbis/http/UserHelper.scala rename to src/main/scala/me/arcanis/ffxivbis/http/helpers/UserHelper.scala index dc6c202..a2b6be0 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/UserHelper.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/helpers/UserHelper.scala @@ -1,17 +1,9 @@ -/* - * Copyright (c) 2019 Evgeniy Alekseev. - * - * This file is part of ffxivbis - * (see https://github.com/arcan1s/ffxivbis). - * - * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause - */ -package me.arcanis.ffxivbis.http +package me.arcanis.ffxivbis.http.helpers import akka.actor.typed.scaladsl.AskPattern.Askable import akka.actor.typed.{ActorRef, Scheduler} import akka.util.Timeout -import me.arcanis.ffxivbis.messages.{AddUser, DeleteUser, GetNewPartyId, GetUser, GetUsers, Message} +import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.models.User import scala.concurrent.Future diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/BasePartyView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/BasePartyView.scala deleted file mode 100644 index 5a9d049..0000000 --- a/src/main/scala/me/arcanis/ffxivbis/http/view/BasePartyView.scala +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (c) 2019 Evgeniy Alekseev. - * - * This file is part of ffxivbis - * (see https://github.com/arcan1s/ffxivbis). - * - * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause - */ -package me.arcanis.ffxivbis.http.view - -import akka.actor.typed.{ActorRef, Scheduler} -import akka.http.scaladsl.model.StatusCodes -import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server._ -import akka.util.Timeout -import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper} -import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} - -import scala.util.{Failure, Success} - -class BasePartyView(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])( - implicit - timeout: Timeout, - scheduler: Scheduler -) extends PlayerHelper - with Authorization { - - def route: Route = getIndex - - def getIndex: Route = - path("party" / Segment) { partyId => - extractExecutionContext { implicit executionContext => - authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => - get { - onComplete(getPartyDescription(partyId)) { - case Success(description) => - complete(StatusCodes.OK, RootView.toHtml(BasePartyView.template(partyId, description.alias))) - case Failure(exception) => throw exception - } - } - } - } - } -} - -object BasePartyView { - import scalatags.Text - import scalatags.Text.all._ - import scalatags.Text.tags2.{title => titleTag} - - def root(partyId: String): Text.TypedTag[String] = - a(href := s"/party/$partyId", title := "root")("root") - - def template(partyId: String, alias: String): String = - "" + - html( - lang := "en", - head( - titleTag(s"Party $alias"), - link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css") - ), - body( - h2(s"Party $alias"), - br, - h2(a(href := s"/party/$partyId/players", title := "party")("party")), - h2(a(href := s"/party/$partyId/bis", title := "bis management")("best in slot")), - h2(a(href := s"/party/$partyId/loot", title := "loot management")("loot")), - h2(a(href := s"/party/$partyId/suggest", title := "suggest loot")("suggest")), - hr, - h2(a(href := s"/party/$partyId/users", title := "user management")("users")) - ) - ) -} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/BiSView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/BiSView.scala deleted file mode 100644 index 57a5c42..0000000 --- a/src/main/scala/me/arcanis/ffxivbis/http/view/BiSView.scala +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright (c) 2019 Evgeniy Alekseev. - * - * This file is part of ffxivbis - * (see https://github.com/arcan1s/ffxivbis). - * - * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause - */ -package me.arcanis.ffxivbis.http.view - -import akka.actor.typed.{ActorRef, Scheduler} -import akka.http.scaladsl.model.StatusCodes -import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server._ -import akka.util.Timeout -import me.arcanis.ffxivbis.http.{Authorization, BiSHelper} -import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} -import me.arcanis.ffxivbis.models.{Piece, PieceType, Player, PlayerId} - -import scala.concurrent.{ExecutionContext, Future} -import scala.util.Try - -class BiSView(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])(implicit - timeout: Timeout, - scheduler: Scheduler -) extends BiSHelper - with Authorization { - - def route: Route = getBiS ~ modifyBiS - - def getBiS: Route = - path("party" / Segment / "bis") { partyId: String => - extractExecutionContext { implicit executionContext => - authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => - get { - complete { - bis(partyId, None) - .map { players => - BiSView.template(partyId, players, None) - } - .map { text => - (StatusCodes.OK, RootView.toHtml(text)) - } - } - } - } - } - } - - def modifyBiS: Route = - path("party" / Segment / "bis") { partyId: String => - extractExecutionContext { implicit executionContext => - authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => - post { - formFields( - "player".as[String], - "piece".as[String].?, - "piece_type".as[String].?, - "link".as[String].?, - "action".as[String] - ) { (player, maybePiece, maybePieceType, maybeLink, action) => - onComplete(modifyBiSCall(partyId, player, maybePiece, maybePieceType, maybeLink, action)) { _ => - redirect(s"/party/$partyId/bis", StatusCodes.Found) - } - } - } - } - } - } - - private def modifyBiSCall( - partyId: String, - player: String, - maybePiece: Option[String], - maybePieceType: Option[String], - maybeLink: Option[String], - action: String - )(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = { - def getPiece(playerId: PlayerId, piece: String, pieceType: String) = - Try(Piece(piece, PieceType.withName(pieceType), playerId.job)).toOption - - def bisAction(playerId: PlayerId, piece: String, pieceType: String)(fn: Piece => Future[Unit]) = - getPiece(playerId, piece, pieceType) match { - case Some(item) => fn(item) - case _ => Future.failed(new Error(s"Could not construct piece from `$piece ($pieceType)`")) - } - - PlayerId(partyId, player) match { - case Some(playerId) => - (maybePiece, maybePieceType, action, maybeLink.map(_.trim).filter(_.nonEmpty)) match { - case (Some(piece), Some(pieceType), "add", _) => - bisAction(playerId, piece, pieceType)(addPieceBiS(playerId, _)) - case (Some(piece), Some(pieceType), "remove", _) => - bisAction(playerId, piece, pieceType)(removePieceBiS(playerId, _)) - case (_, _, "create", Some(link)) => putBiS(playerId, link) - case _ => Future.failed(new Error(s"Could not perform $action")) - } - case _ => Future.failed(new Error(s"Could not construct player id from `$player`")) - } - } -} - -object BiSView { - import scalatags.Text.all._ - import scalatags.Text.tags2.{title => titleTag} - - def template(partyId: String, party: Seq[Player], error: Option[String]): String = - "" + - html( - lang := "en", - head( - titleTag("Best in slot"), - link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css") - ), - body( - h2("Best in slot"), - ErrorView.template(error), - SearchLineView.template, - form(action := s"/party/$partyId/bis", method := "post")( - select(name := "player", id := "player", title := "player")( - for (player <- party) yield option(player.playerId.toString) - ), - select(name := "piece", id := "piece", title := "piece")( - for (piece <- Piece.available) yield option(piece) - ), - select(name := "piece_type", id := "piece_type", title := "piece type")( - for (pieceType <- PieceType.available) yield option(pieceType.toString) - ), - input(name := "action", id := "action", `type` := "hidden", value := "add"), - input(name := "add", id := "add", `type` := "submit", value := "add") - ), - form(action := s"/party/$partyId/bis", method := "post")( - select(name := "player", id := "player", title := "player")( - for (player <- party) yield option(player.playerId.toString) - ), - input(name := "link", id := "link", placeholder := "player bis link", title := "link", `type` := "text"), - input(name := "action", id := "action", `type` := "hidden", value := "create"), - input(name := "add", id := "add", `type` := "submit", value := "add") - ), - table(id := "result")( - tr( - th("player"), - th("piece"), - th("piece type"), - th("") - ), - for (player <- party; piece <- player.bis.pieces) - yield tr( - td(`class` := "include_search")(player.playerId.toString), - td(`class` := "include_search")(piece.piece), - td(piece.pieceType.toString), - td( - form(action := s"/party/$partyId/bis", method := "post")( - input(name := "player", id := "player", `type` := "hidden", value := player.playerId.toString), - input(name := "piece", id := "piece", `type` := "hidden", value := piece.piece), - input( - name := "piece_type", - id := "piece_type", - `type` := "hidden", - value := piece.pieceType.toString - ), - input(name := "action", id := "action", `type` := "hidden", value := "remove"), - input(name := "remove", id := "remove", `type` := "submit", value := "x") - ) - ) - ) - ), - ExportToCSVView.template, - BasePartyView.root(partyId), - script(src := "/static/table_search.js", `type` := "text/javascript") - ) - ) -} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/ErrorView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/ErrorView.scala deleted file mode 100644 index 9a35e81..0000000 --- a/src/main/scala/me/arcanis/ffxivbis/http/view/ErrorView.scala +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (c) 2019 Evgeniy Alekseev. - * - * This file is part of ffxivbis - * (see https://github.com/arcan1s/ffxivbis). - * - * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause - */ -package me.arcanis.ffxivbis.http.view - -import scalatags.Text -import scalatags.Text.all._ - -object ErrorView { - - def template(error: Option[String]): Text.TypedTag[String] = error match { - case Some(text) => p(id := "error", s"Error occurs: $text") - case None => p("") - } -} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/ExportToCSVView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/ExportToCSVView.scala deleted file mode 100644 index 0b76606..0000000 --- a/src/main/scala/me/arcanis/ffxivbis/http/view/ExportToCSVView.scala +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (c) 2019 Evgeniy Alekseev. - * - * This file is part of ffxivbis - * (see https://github.com/arcan1s/ffxivbis). - * - * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause - */ -package me.arcanis.ffxivbis.http.view - -import scalatags.Text -import scalatags.Text.all._ - -object ExportToCSVView { - - def template: Text.TypedTag[String] = - div( - button(onclick := "exportTableToCsv('result.csv')")("Export to CSV"), - script(src := "/static/table_export.js", `type` := "text/javascript") - ) -} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/IndexView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/IndexView.scala deleted file mode 100644 index 97d772f..0000000 --- a/src/main/scala/me/arcanis/ffxivbis/http/view/IndexView.scala +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (c) 2019 Evgeniy Alekseev. - * - * This file is part of ffxivbis - * (see https://github.com/arcan1s/ffxivbis). - * - * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause - */ -package me.arcanis.ffxivbis.http.view - -import akka.actor.typed.{ActorRef, Scheduler} -import akka.http.scaladsl.model.StatusCodes -import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server._ -import akka.util.Timeout -import me.arcanis.ffxivbis.http.{PlayerHelper, UserHelper} -import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} -import me.arcanis.ffxivbis.models.{PartyDescription, Permission, User} - -import scala.concurrent.Future -import scala.util.{Failure, Success} - -class IndexView(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])(implicit - timeout: Timeout, - scheduler: Scheduler -) extends PlayerHelper - with UserHelper { - - def route: Route = createParty ~ getIndex - - def createParty: Route = - path("party") { - extractExecutionContext { implicit executionContext => - post { - formFields("username".as[String], "password".as[String], "alias".as[String].?) { - (username, password, maybeAlias) => - onComplete { - newPartyId.flatMap { partyId => - val user = User(partyId, username, password, Permission.admin) - addUser(user, isHashedPassword = false).flatMap { _ => - if (maybeAlias.getOrElse("").isEmpty) Future.successful(partyId) - else updateDescription(PartyDescription(partyId, maybeAlias)).map(_ => partyId) - } - } - } { - case Success(partyId) => redirect(s"/party/$partyId", StatusCodes.Found) - case Failure(exception) => throw exception - } - } - } - } - } - - def getIndex: Route = - pathEndOrSingleSlash { - get { - parameters("partyId".as[String].?) { - case Some(partyId) => redirect(s"/party/$partyId", StatusCodes.Found) - case _ => complete(StatusCodes.OK, RootView.toHtml(IndexView.template)) - } - } - } -} - -object IndexView { - import scalatags.Text.all._ - import scalatags.Text.tags2.{title => titleTag} - - def template: String = - "" + - html( - head( - titleTag("FFXIV loot helper"), - link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css") - ), - body( - form(action := s"party", method := "post")( - label("create a new party"), - input(name := "alias", id := "alias", placeholder := "party alias", title := "alias", `type` := "text"), - input( - name := "username", - id := "username", - placeholder := "username", - title := "username", - `type` := "text" - ), - input( - name := "password", - id := "password", - placeholder := "password", - title := "password", - `type` := "password" - ), - input(name := "add", id := "add", `type` := "submit", value := "add") - ), - br, - form(action := "/", method := "get")( - label("already have party?"), - input(name := "partyId", id := "partyId", placeholder := "party id", title := "party id", `type` := "text"), - input(name := "go", id := "go", `type` := "submit", value := "go") - ) - ) - ) -} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/LootSuggestView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/LootSuggestView.scala deleted file mode 100644 index a15d023..0000000 --- a/src/main/scala/me/arcanis/ffxivbis/http/view/LootSuggestView.scala +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright (c) 2019 Evgeniy Alekseev. - * - * This file is part of ffxivbis - * (see https://github.com/arcan1s/ffxivbis). - * - * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause - */ -package me.arcanis.ffxivbis.http.view - -import akka.actor.typed.{ActorRef, Scheduler} -import akka.http.scaladsl.model.StatusCodes -import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server.Route -import akka.util.Timeout -import me.arcanis.ffxivbis.http.{Authorization, LootHelper} -import me.arcanis.ffxivbis.messages.Message -import me.arcanis.ffxivbis.models.{Job, Piece, PieceType, PlayerIdWithCounters} - -import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Success, Try} - -class LootSuggestView(override val storage: ActorRef[Message])(implicit timeout: Timeout, scheduler: Scheduler) - extends LootHelper - with Authorization { - - def route: Route = getIndex ~ suggestLoot - - def getIndex: Route = - path("party" / Segment / "suggest") { partyId: String => - extractExecutionContext { implicit executionContext => - authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => - get { - complete { - val text = LootSuggestView.template(partyId, Seq.empty, None, false, None) - (StatusCodes.OK, RootView.toHtml(text)) - } - } - } - } - } - - def suggestLoot: Route = - path("party" / Segment / "suggest") { partyId: String => - extractExecutionContext { implicit executionContext => - authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => - post { - formFields("piece".as[String], "job".as[String], "piece_type".as[String], "free_loot".as[String].?) { - (piece, job, pieceType, maybeFreeLoot) => - import me.arcanis.ffxivbis.utils.Implicits._ - - val maybePiece = Try(Piece(piece, PieceType.withName(pieceType), Job.withName(job))).toOption - - onComplete(suggestLootCall(partyId, maybePiece)) { - case Success(players) => - val text = LootSuggestView.template(partyId, players, maybePiece, maybeFreeLoot, None) - complete(StatusCodes.OK, RootView.toHtml(text)) - case Failure(exception) => - val text = LootSuggestView.template(partyId, Seq.empty, None, false, Some(exception.getMessage)) - complete(StatusCodes.OK, RootView.toHtml(text)) - } - } - } - } - } - } - - private def suggestLootCall(partyId: String, maybePiece: Option[Piece])(implicit - executionContext: ExecutionContext, - timeout: Timeout - ): Future[Seq[PlayerIdWithCounters]] = - maybePiece match { - case Some(piece) => suggestPiece(partyId, piece) - case _ => Future.failed(new Error(s"Could not construct piece from `$maybePiece`")) - } -} - -object LootSuggestView { - import scalatags.Text.all._ - import scalatags.Text.tags2.{title => titleTag} - - def template( - partyId: String, - party: Seq[PlayerIdWithCounters], - piece: Option[Piece], - isFreeLoot: Boolean, - error: Option[String] - ): String = - "" + - html( - lang := "en", - head( - titleTag("Suggest loot"), - link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css") - ), - body( - h2("Suggest loot"), - for (part <- piece) yield p(s"Piece ${part.piece} (${part.pieceType})"), - ErrorView.template(error), - SearchLineView.template, - form(action := s"/party/$partyId/suggest", method := "post")( - select(name := "piece", id := "piece", title := "piece")( - for (piece <- Piece.available) yield option(piece) - ), - select(name := "job", id := "job", title := "job")( - for (job <- Job.availableWithAnyJob) yield option(job.toString) - ), - select(name := "piece_type", id := "piece_type", title := "piece type")( - for (pieceType <- PieceType.available) yield option(pieceType.toString) - ), - input(name := "free_loot", id := "free_loot", title := "is free loot", `type` := "checkbox"), - label(`for` := "free_loot")("is free loot"), - input(name := "suggest", id := "suggest", `type` := "submit", value := "suggest") - ), - table(id := "result")( - tr( - th("player"), - th("is required"), - th("these pieces looted"), - th("total bis pieces looted"), - th("total pieces looted"), - th("") - ), - for (player <- party) - yield tr( - td(`class` := "include_search")(player.playerId.toString), - td(player.isRequiredToString), - td(player.lootCount), - td(player.lootCountBiS), - td(player.lootCountTotal), - td( - form(action := s"/party/$partyId/loot", method := "post")( - input(name := "player", id := "player", `type` := "hidden", value := player.playerId.toString), - input( - name := "piece", - id := "piece", - `type` := "hidden", - value := piece.map(_.piece).getOrElse("") - ), - input( - name := "piece_type", - id := "piece_type", - `type` := "hidden", - value := piece.map(_.pieceType.toString).getOrElse("") - ), - input( - name := "free_loot", - id := "free_loot", - `type` := "hidden", - value := (if (isFreeLoot) "yes" else "no") - ), - input(name := "action", id := "action", `type` := "hidden", value := "add"), - input(name := "add", id := "add", `type` := "submit", value := "add") - ) - ) - ) - ), - ExportToCSVView.template, - BasePartyView.root(partyId), - script(src := "/static/table_search.js", `type` := "text/javascript") - ) - ) -} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/LootView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/LootView.scala deleted file mode 100644 index a4d6ef2..0000000 --- a/src/main/scala/me/arcanis/ffxivbis/http/view/LootView.scala +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright (c) 2019 Evgeniy Alekseev. - * - * This file is part of ffxivbis - * (see https://github.com/arcan1s/ffxivbis). - * - * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause - */ -package me.arcanis.ffxivbis.http.view - -import akka.actor.typed.{ActorRef, Scheduler} -import akka.http.scaladsl.model.StatusCodes -import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server.Route -import akka.util.Timeout -import me.arcanis.ffxivbis.http.{Authorization, LootHelper} -import me.arcanis.ffxivbis.messages.Message -import me.arcanis.ffxivbis.models.{Piece, PieceType, Player, PlayerId} - -import scala.concurrent.{ExecutionContext, Future} -import scala.util.Try - -class LootView(override val storage: ActorRef[Message])(implicit timeout: Timeout, scheduler: Scheduler) - extends LootHelper - with Authorization { - - def route: Route = getLoot ~ modifyLoot - - def getLoot: Route = - path("party" / Segment / "loot") { partyId: String => - extractExecutionContext { implicit executionContext => - authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => - get { - complete { - loot(partyId, None) - .map { players => - LootView.template(partyId, players, None) - } - .map { text => - (StatusCodes.OK, RootView.toHtml(text)) - } - } - } - } - } - } - - def modifyLoot: Route = - path("party" / Segment / "loot") { partyId: String => - extractExecutionContext { implicit executionContext => - authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => - post { - formFields( - "player".as[String], - "piece".as[String], - "piece_type".as[String], - "action".as[String], - "free_loot".as[String].? - ) { (player, piece, pieceType, action, isFreeLoot) => - onComplete(modifyLootCall(partyId, player, piece, pieceType, isFreeLoot, action)) { _ => - redirect(s"/party/$partyId/loot", StatusCodes.Found) - } - } - } - } - } - } - - private def modifyLootCall( - partyId: String, - player: String, - maybePiece: String, - maybePieceType: String, - maybeFreeLoot: Option[String], - action: String - )(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = { - import me.arcanis.ffxivbis.utils.Implicits._ - - def getPiece(playerId: PlayerId) = - Try(Piece(maybePiece, PieceType.withName(maybePieceType), playerId.job)).toOption - - PlayerId(partyId, player) match { - case Some(playerId) => - (getPiece(playerId), action) match { - case (Some(piece), "add") => addPieceLoot(playerId, piece, maybeFreeLoot) - case (Some(piece), "remove") => removePieceLoot(playerId, piece) - case _ => Future.failed(new Error(s"Could not construct piece from `$maybePiece ($maybePieceType)`")) - } - case _ => Future.failed(new Error(s"Could not construct player id from `$player`")) - } - } -} - -object LootView { - import scalatags.Text.all._ - import scalatags.Text.tags2.{title => titleTag} - - def template(partyId: String, party: Seq[Player], error: Option[String]): String = - "" + - html( - lang := "en", - head( - titleTag("Loot"), - link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css") - ), - body( - h2("Loot"), - ErrorView.template(error), - SearchLineView.template, - form(action := s"/party/$partyId/loot", method := "post")( - select(name := "player", id := "player", title := "player")( - for (player <- party) yield option(player.playerId.toString) - ), - select(name := "piece", id := "piece", title := "piece")( - for (piece <- Piece.available) yield option(piece) - ), - select(name := "piece_type", id := "piece_type", title := "piece type")( - for (pieceType <- PieceType.available) yield option(pieceType.toString) - ), - input(name := "free_loot", id := "free_loot", title := "is free loot", `type` := "checkbox"), - label(`for` := "free_loot")("is free loot"), - input(name := "action", id := "action", `type` := "hidden", value := "add"), - input(name := "add", id := "add", `type` := "submit", value := "add") - ), - table(id := "result")( - tr( - th("player"), - th("piece"), - th("piece type"), - th("is free loot"), - th("timestamp"), - th("") - ), - for (player <- party; loot <- player.loot) - yield tr( - td(`class` := "include_search")(player.playerId.toString), - td(`class` := "include_search")(loot.piece.piece), - td(loot.piece.pieceType.toString), - td(loot.isFreeLootToString), - td(loot.timestamp.toString), - td( - form(action := s"/party/$partyId/loot", method := "post")( - input(name := "player", id := "player", `type` := "hidden", value := player.playerId.toString), - input(name := "piece", id := "piece", `type` := "hidden", value := loot.piece.piece), - input( - name := "piece_type", - id := "piece_type", - `type` := "hidden", - value := loot.piece.pieceType.toString - ), - input(name := "free_loot", id := "free_loot", `type` := "hidden", value := loot.isFreeLootToString), - input(name := "action", id := "action", `type` := "hidden", value := "remove"), - input(name := "remove", id := "remove", `type` := "submit", value := "x") - ) - ) - ) - ), - ExportToCSVView.template, - BasePartyView.root(partyId), - script(src := "/static/table_search.js", `type` := "text/javascript") - ) - ) - -} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/PlayerView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/PlayerView.scala deleted file mode 100644 index 25c850d..0000000 --- a/src/main/scala/me/arcanis/ffxivbis/http/view/PlayerView.scala +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright (c) 2019 Evgeniy Alekseev. - * - * This file is part of ffxivbis - * (see https://github.com/arcan1s/ffxivbis). - * - * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause - */ -package me.arcanis.ffxivbis.http.view - -import akka.actor.typed.{ActorRef, Scheduler} -import akka.http.scaladsl.model.StatusCodes -import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server.Route -import akka.util.Timeout -import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper} -import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} -import me.arcanis.ffxivbis.models._ - -import scala.concurrent.{ExecutionContext, Future} - -class PlayerView(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])(implicit - timeout: Timeout, - scheduler: Scheduler -) extends PlayerHelper - with Authorization { - - def route: Route = getParty ~ modifyParty - - def getParty: Route = - path("party" / Segment / "players") { partyId: String => - extractExecutionContext { implicit executionContext => - authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => - get { - complete { - getPlayers(partyId, None) - .map { players => - PlayerView.template(partyId, players.map(_.withCounters(None)), None) - } - .map { text => - (StatusCodes.OK, RootView.toHtml(text)) - } - } - } - } - } - } - - def modifyParty: Route = - path("party" / Segment / "players") { partyId: String => - extractExecutionContext { implicit executionContext => - authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => - post { - formFields( - "nick".as[String], - "job".as[String], - "priority".as[Int].?, - "link".as[String].?, - "action".as[String] - ) { (nick, job, maybePriority, maybeLink, action) => - onComplete(modifyPartyCall(partyId, nick, job, maybePriority, maybeLink, action)) { _ => - redirect(s"/party/$partyId/players", StatusCodes.Found) - } - } - } - } - } - } - - private def modifyPartyCall( - partyId: String, - nick: String, - job: String, - maybePriority: Option[Int], - maybeLink: Option[String], - action: String - )(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = { - def maybePlayerId = PlayerId(partyId, Some(nick), Some(job)) - def player(playerId: PlayerId) = - Player(-1, partyId, playerId.job, playerId.nick, BiS.empty, Seq.empty, maybeLink, maybePriority.getOrElse(0)) - - (action, maybePlayerId) match { - case ("add", Some(playerId)) => addPlayer(player(playerId)) - case ("remove", Some(playerId)) => removePlayer(playerId) - case _ => Future.failed(new Error(s"Could not perform $action with $nick ($job)")) - } - } -} - -object PlayerView { - import scalatags.Text.all._ - import scalatags.Text.tags2.{title => titleTag} - - def template(partyId: String, party: Seq[PlayerIdWithCounters], error: Option[String]): String = - "" + - html( - lang := "en", - head( - titleTag("Party"), - link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css") - ), - body( - h2("Party"), - ErrorView.template(error), - SearchLineView.template, - form(action := s"/party/$partyId/players", method := "post")( - input(name := "nick", id := "nick", placeholder := "nick", title := "nick", `type` := "nick"), - select(name := "job", id := "job", title := "job")(for (job <- Job.available) yield option(job.toString)), - input(name := "link", id := "link", placeholder := "player bis link", title := "link", `type` := "text"), - input( - name := "prioiry", - id := "priority", - placeholder := "priority", - title := "priority", - `type` := "number", - value := "0" - ), - input(name := "action", id := "action", `type` := "hidden", value := "add"), - input(name := "add", id := "add", `type` := "submit", value := "add") - ), - table(id := "result")( - tr( - th("nick"), - th("job"), - th("total bis pieces looted"), - th("total pieces looted"), - th("priority"), - th("") - ), - for (player <- party) - yield tr( - td(`class` := "include_search")(player.nick), - td(`class` := "include_search")(player.job.toString), - td(player.lootCountBiS), - td(player.lootCountTotal), - td(player.priority), - td( - form(action := s"/party/$partyId/players", method := "post")( - input(name := "nick", id := "nick", `type` := "hidden", value := player.nick), - input(name := "job", id := "job", `type` := "hidden", value := player.job.toString), - input(name := "action", id := "action", `type` := "hidden", value := "remove"), - input(name := "remove", id := "remove", `type` := "submit", value := "x") - ) - ) - ) - ), - ExportToCSVView.template, - BasePartyView.root(partyId), - script(src := "/static/table_search.js", `type` := "text/javascript") - ) - ) -} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/RootView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/RootView.scala index 62e3fed..afc3bca 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/view/RootView.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/view/RootView.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -8,34 +8,73 @@ */ package me.arcanis.ffxivbis.http.view -import akka.actor.typed.{ActorRef, Scheduler} -import akka.http.scaladsl.model.{ContentTypes, HttpEntity} +import akka.http.scaladsl.model.headers.RawHeader import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route -import akka.util.Timeout -import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} +import me.arcanis.ffxivbis.http.{Authorization, AuthorizationProvider} -class RootView(storage: ActorRef[Message], provider: ActorRef[BiSProviderMessage])(implicit - timeout: Timeout, - scheduler: Scheduler -) { +class RootView(override val auth: AuthorizationProvider) extends Authorization { - private val basePartyView = new BasePartyView(storage, provider) - private val indexView = new IndexView(storage, provider) + def route: Route = getBiS ~ getIndex ~ getLoot ~ getParty ~ getUsers - private val biSView = new BiSView(storage, provider) - private val lootView = new LootView(storage) - private val lootSuggestView = new LootSuggestView(storage) - private val playerView = new PlayerView(storage, provider) - private val userView = new UserView(storage) + def getBiS: Route = + path("party" / Segment / "bis") { partyId: String => + extractExecutionContext { implicit executionContext => + authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { user => + respondWithHeaders( + RawHeader("X-Party-Id", partyId), + RawHeader("X-User-Permission", user.permission.toString) + ) { + getFromResource("html/bis.html") + } + } + } + } - def route: Route = - basePartyView.route ~ indexView.route ~ - biSView.route ~ lootView.route ~ lootSuggestView.route ~ playerView.route ~ userView.route -} - -object RootView { - - def toHtml(template: String): HttpEntity.Strict = - HttpEntity(ContentTypes.`text/html(UTF-8)`, template) + def getIndex: Route = + pathEndOrSingleSlash { + getFromResource("html/index.html") + } + + def getLoot: Route = + path("party" / Segment / "loot") { partyId: String => + extractExecutionContext { implicit executionContext => + authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { user => + respondWithHeaders( + RawHeader("X-Party-Id", partyId), + RawHeader("X-User-Permission", user.permission.toString) + ) { + getFromResource("html/loot.html") + } + } + } + } + + def getParty: Route = + path("party" / Segment) { partyId: String => + extractExecutionContext { implicit executionContext => + authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { user => + respondWithHeaders( + RawHeader("X-Party-Id", partyId), + RawHeader("X-User-Permission", user.permission.toString) + ) { + getFromResource("html/party.html") + } + } + } + } + + def getUsers: Route = + path("party" / Segment / "users") { partyId: String => + extractExecutionContext { implicit executionContext => + authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { user => + respondWithHeaders( + RawHeader("X-Party-Id", partyId), + RawHeader("X-User-Permission", user.permission.toString) + ) { + getFromResource("html/users.html") + } + } + } + } } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/SearchLineView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/SearchLineView.scala deleted file mode 100644 index ef2274d..0000000 --- a/src/main/scala/me/arcanis/ffxivbis/http/view/SearchLineView.scala +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) 2019 Evgeniy Alekseev. - * - * This file is part of ffxivbis - * (see https://github.com/arcan1s/ffxivbis). - * - * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause - */ -package me.arcanis.ffxivbis.http.view - -import scalatags.Text -import scalatags.Text.all._ - -object SearchLineView { - - def template: Text.TypedTag[String] = - div( - input( - `type` := "text", - id := "search", - onkeyup := "searchTable()", - placeholder := "search for data", - title := "search" - ) - ) -} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/UserView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/UserView.scala deleted file mode 100644 index 68c7536..0000000 --- a/src/main/scala/me/arcanis/ffxivbis/http/view/UserView.scala +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright (c) 2019 Evgeniy Alekseev. - * - * This file is part of ffxivbis - * (see https://github.com/arcan1s/ffxivbis). - * - * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause - */ -package me.arcanis.ffxivbis.http.view - -import akka.actor.typed.{ActorRef, Scheduler} -import akka.http.scaladsl.model.StatusCodes -import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server.Route -import akka.util.Timeout -import me.arcanis.ffxivbis.http.{Authorization, UserHelper} -import me.arcanis.ffxivbis.messages.Message -import me.arcanis.ffxivbis.models.{Permission, User} - -import scala.concurrent.{ExecutionContext, Future} -import scala.util.Try - -class UserView(override val storage: ActorRef[Message])(implicit timeout: Timeout, scheduler: Scheduler) - extends UserHelper - with Authorization { - - def route: Route = getUsers ~ modifyUsers - - def getUsers: Route = - path("party" / Segment / "users") { partyId: String => - extractExecutionContext { implicit executionContext => - authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ => - get { - complete { - users(partyId) - .map { users => - UserView.template(partyId, users, None) - } - .map { text => - (StatusCodes.OK, RootView.toHtml(text)) - } - } - } - } - } - } - - def modifyUsers: Route = - path("party" / Segment / "users") { partyId: String => - extractExecutionContext { implicit executionContext => - authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ => - post { - formFields("username".as[String], "password".as[String].?, "permission".as[String].?, "action".as[String]) { - (username, maybePassword, maybePermission, action) => - onComplete(modifyUsersCall(partyId, username, maybePassword, maybePermission, action)) { case _ => - redirect(s"/party/$partyId/users", StatusCodes.Found) - } - } - } - } - } - } - - private def modifyUsersCall( - partyId: String, - username: String, - maybePassword: Option[String], - maybePermission: Option[String], - action: String - )(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = { - def permission: Option[Permission.Value] = - maybePermission.flatMap(p => Try(Permission.withName(p)).toOption) - - action match { - case "add" => - (maybePassword, permission) match { - case (Some(password), Some(permission)) => - addUser(User(partyId, username, password, permission), isHashedPassword = false) - case _ => - Future.failed( - new Error(s"Could not construct permission/password from `$maybePermission`/`$maybePassword`") - ) - } - case "remove" => removeUser(partyId, username) - case _ => Future.failed(new Error(s"Could not perform $action")) - } - } -} - -object UserView { - import scalatags.Text.all._ - import scalatags.Text.tags2.{title => titleTag} - - def template(partyId: String, users: Seq[User], error: Option[String]) = - "" + - html( - lang := "en", - head( - titleTag("Users"), - link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css") - ), - body( - h2("Users"), - ErrorView.template(error), - SearchLineView.template, - form(action := s"/party/$partyId/users", method := "post")( - input( - name := "username", - id := "username", - placeholder := "username", - title := "username", - `type` := "text" - ), - input( - name := "password", - id := "password", - placeholder := "password", - title := "password", - `type` := "password" - ), - select(name := "permission", id := "permission", title := "permission")(option("get"), option("post")), - input(name := "action", id := "action", `type` := "hidden", value := "add"), - input(name := "add", id := "add", `type` := "submit", value := "add") - ), - table(id := "result")( - tr( - th("username"), - th("permission"), - th("") - ), - for (user <- users) - yield tr( - td(`class` := "include_search")(user.username), - td(user.permission.toString), - td( - form(action := s"/party/$partyId/users", method := "post")( - input(name := "username", id := "username", `type` := "hidden", value := user.username.toString), - input(name := "action", id := "action", `type` := "hidden", value := "remove"), - input(name := "remove", id := "remove", `type` := "submit", value := "x") - ) - ) - ) - ), - ExportToCSVView.template, - BasePartyView.root(partyId), - script(src := "/static/table_search.js", `type` := "text/javascript") - ) - ) -} diff --git a/src/main/scala/me/arcanis/ffxivbis/messages/BiSProviderMessage.scala b/src/main/scala/me/arcanis/ffxivbis/messages/BiSProviderMessage.scala index daa5c48..703c9fc 100644 --- a/src/main/scala/me/arcanis/ffxivbis/messages/BiSProviderMessage.scala +++ b/src/main/scala/me/arcanis/ffxivbis/messages/BiSProviderMessage.scala @@ -1,3 +1,11 @@ +/* + * Copyright (c) 2019-2022 Evgeniy Alekseev. + * + * This file is part of ffxivbis + * (see https://github.com/arcan1s/ffxivbis). + * + * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause + */ package me.arcanis.ffxivbis.messages import akka.actor.typed.ActorRef diff --git a/src/main/scala/me/arcanis/ffxivbis/messages/ContolMessage.scala b/src/main/scala/me/arcanis/ffxivbis/messages/ContolMessage.scala index 409d399..cdceda5 100644 --- a/src/main/scala/me/arcanis/ffxivbis/messages/ContolMessage.scala +++ b/src/main/scala/me/arcanis/ffxivbis/messages/ContolMessage.scala @@ -1,3 +1,11 @@ +/* + * Copyright (c) 2019-2022 Evgeniy Alekseev. + * + * This file is part of ffxivbis + * (see https://github.com/arcan1s/ffxivbis). + * + * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause + */ package me.arcanis.ffxivbis.messages import akka.actor.typed.ActorRef diff --git a/src/main/scala/me/arcanis/ffxivbis/messages/DatabaseMessage.scala b/src/main/scala/me/arcanis/ffxivbis/messages/DatabaseMessage.scala index 07e16ed..72ff50e 100644 --- a/src/main/scala/me/arcanis/ffxivbis/messages/DatabaseMessage.scala +++ b/src/main/scala/me/arcanis/ffxivbis/messages/DatabaseMessage.scala @@ -1,7 +1,15 @@ +/* + * Copyright (c) 2019-2022 Evgeniy Alekseev. + * + * This file is part of ffxivbis + * (see https://github.com/arcan1s/ffxivbis). + * + * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause + */ package me.arcanis.ffxivbis.messages import akka.actor.typed.{ActorRef, Behavior} -import me.arcanis.ffxivbis.models.{Party, PartyDescription, Piece, Player, PlayerId, User} +import me.arcanis.ffxivbis.models._ import me.arcanis.ffxivbis.service.LootSelector sealed trait DatabaseMessage extends Message { @@ -15,65 +23,75 @@ object DatabaseMessage { } // bis handler -case class AddPieceToBis(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends DatabaseMessage { +trait BisDatabaseMessage extends DatabaseMessage + +case class AddPieceToBis(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends BisDatabaseMessage { override def partyId: String = playerId.partyId } -case class GetBiS(partyId: String, playerId: Option[PlayerId], replyTo: ActorRef[Seq[Player]]) extends DatabaseMessage +case class GetBiS(partyId: String, playerId: Option[PlayerId], replyTo: ActorRef[Seq[Player]]) + extends BisDatabaseMessage -case class RemovePieceFromBiS(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends DatabaseMessage { +case class RemovePieceFromBiS(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends BisDatabaseMessage { override def partyId: String = playerId.partyId } -case class RemovePiecesFromBiS(playerId: PlayerId, replyTo: ActorRef[Unit]) extends DatabaseMessage { +case class RemovePiecesFromBiS(playerId: PlayerId, replyTo: ActorRef[Unit]) extends BisDatabaseMessage { override def partyId: String = playerId.partyId } // loot handler +trait LootDatabaseMessage extends DatabaseMessage + case class AddPieceTo(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean, replyTo: ActorRef[Unit]) - extends DatabaseMessage { + extends LootDatabaseMessage { override def partyId: String = playerId.partyId } -case class GetLoot(partyId: String, playerId: Option[PlayerId], replyTo: ActorRef[Seq[Player]]) extends DatabaseMessage +case class GetLoot(partyId: String, playerId: Option[PlayerId], replyTo: ActorRef[Seq[Player]]) + extends LootDatabaseMessage -case class RemovePieceFrom(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends DatabaseMessage { +case class RemovePieceFrom(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends LootDatabaseMessage { override def partyId: String = playerId.partyId } case class SuggestLoot(partyId: String, piece: Piece, replyTo: ActorRef[LootSelector.LootSelectorResult]) - extends DatabaseMessage + extends LootDatabaseMessage // party handler -case class AddPlayer(player: Player, replyTo: ActorRef[Unit]) extends DatabaseMessage { +trait PartyDatabaseMessage extends DatabaseMessage + +case class AddPlayer(player: Player, replyTo: ActorRef[Unit]) extends PartyDatabaseMessage { override def partyId: String = player.partyId } -case class GetParty(partyId: String, replyTo: ActorRef[Party]) extends DatabaseMessage +case class GetParty(partyId: String, replyTo: ActorRef[Party]) extends PartyDatabaseMessage -case class GetPartyDescription(partyId: String, replyTo: ActorRef[PartyDescription]) extends DatabaseMessage +case class GetPartyDescription(partyId: String, replyTo: ActorRef[PartyDescription]) extends PartyDatabaseMessage -case class GetPlayer(playerId: PlayerId, replyTo: ActorRef[Option[Player]]) extends DatabaseMessage { +case class GetPlayer(playerId: PlayerId, replyTo: ActorRef[Option[Player]]) extends PartyDatabaseMessage { override def partyId: String = playerId.partyId } -case class RemovePlayer(playerId: PlayerId, replyTo: ActorRef[Unit]) extends DatabaseMessage { +case class RemovePlayer(playerId: PlayerId, replyTo: ActorRef[Unit]) extends PartyDatabaseMessage { override def partyId: String = playerId.partyId } -case class UpdateParty(partyDescription: PartyDescription, replyTo: ActorRef[Unit]) extends DatabaseMessage { +case class UpdateParty(partyDescription: PartyDescription, replyTo: ActorRef[Unit]) extends PartyDatabaseMessage { override def partyId: String = partyDescription.partyId } // user handler -case class AddUser(user: User, isHashedPassword: Boolean, replyTo: ActorRef[Unit]) extends DatabaseMessage { +trait UserDatabaseMessage extends DatabaseMessage + +case class AddUser(user: User, isHashedPassword: Boolean, replyTo: ActorRef[Unit]) extends UserDatabaseMessage { override def partyId: String = user.partyId } -case class DeleteUser(partyId: String, username: String, replyTo: ActorRef[Unit]) extends DatabaseMessage +case class DeleteUser(partyId: String, username: String, replyTo: ActorRef[Unit]) extends UserDatabaseMessage -case class Exists(partyId: String, replyTo: ActorRef[Boolean]) extends DatabaseMessage +case class Exists(partyId: String, replyTo: ActorRef[Boolean]) extends UserDatabaseMessage -case class GetUser(partyId: String, username: String, replyTo: ActorRef[Option[User]]) extends DatabaseMessage +case class GetUser(partyId: String, username: String, replyTo: ActorRef[Option[User]]) extends UserDatabaseMessage -case class GetUsers(partyId: String, replyTo: ActorRef[Seq[User]]) extends DatabaseMessage +case class GetUsers(partyId: String, replyTo: ActorRef[Seq[User]]) extends UserDatabaseMessage diff --git a/src/main/scala/me/arcanis/ffxivbis/messages/Message.scala b/src/main/scala/me/arcanis/ffxivbis/messages/Message.scala index def973d..fe392f2 100644 --- a/src/main/scala/me/arcanis/ffxivbis/messages/Message.scala +++ b/src/main/scala/me/arcanis/ffxivbis/messages/Message.scala @@ -1,3 +1,11 @@ +/* + * Copyright (c) 2019-2022 Evgeniy Alekseev. + * + * This file is part of ffxivbis + * (see https://github.com/arcan1s/ffxivbis). + * + * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause + */ package me.arcanis.ffxivbis.messages import akka.actor.typed.Behavior @@ -5,5 +13,6 @@ import akka.actor.typed.Behavior trait Message object Message { + type Handler = PartialFunction[Message, Behavior[Message]] } diff --git a/src/main/scala/me/arcanis/ffxivbis/models/BiS.scala b/src/main/scala/me/arcanis/ffxivbis/models/BiS.scala index 9fd5d64..7eb3e29 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/BiS.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/BiS.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -25,6 +25,7 @@ case class BiS(pieces: Seq[Piece]) { .withDefaultValue(0) def withPiece(piece: Piece): BiS = copy(pieces :+ piece) + def withoutPiece(piece: Piece): BiS = copy(pieces.filterNot(_.strictEqual(piece))) override def equals(obj: Any): Boolean = { diff --git a/src/main/scala/me/arcanis/ffxivbis/models/Job.scala b/src/main/scala/me/arcanis/ffxivbis/models/Job.scala index 2afb6fc..df4a4aa 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/Job.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/Job.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -29,6 +29,7 @@ object Job { sealed trait Job extends Equals { def leftSide: LeftSide + def rightSide: RightSide // conversion to string to avoid recursion diff --git a/src/main/scala/me/arcanis/ffxivbis/models/Loot.scala b/src/main/scala/me/arcanis/ffxivbis/models/Loot.scala index 1f1af48..afcedd2 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/Loot.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/Loot.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). diff --git a/src/main/scala/me/arcanis/ffxivbis/models/Party.scala b/src/main/scala/me/arcanis/ffxivbis/models/Party.scala index a2e5950..62a4330 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/Party.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/Party.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -17,10 +17,13 @@ import scala.util.Random case class Party(partyDescription: PartyDescription, rules: Seq[String], players: Map[PlayerId, Player]) extends StrictLogging { + require(players.keys.forall(_.partyId == partyDescription.partyId), "party id must be same") def getPlayers: Seq[Player] = players.values.toSeq + def player(playerId: PlayerId): Option[Player] = players.get(playerId) + def withPlayer(player: Player): Party = try { require(player.partyId == partyDescription.partyId, "player must belong to this party") diff --git a/src/main/scala/me/arcanis/ffxivbis/models/PartyDescription.scala b/src/main/scala/me/arcanis/ffxivbis/models/PartyDescription.scala index 9da1f4e..a138856 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/PartyDescription.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/PartyDescription.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). diff --git a/src/main/scala/me/arcanis/ffxivbis/models/Piece.scala b/src/main/scala/me/arcanis/ffxivbis/models/Piece.scala index 7dd4603..87d3af0 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/Piece.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/Piece.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -11,7 +11,9 @@ package me.arcanis.ffxivbis.models sealed trait Piece extends Equals { def pieceType: PieceType.PieceType + def job: Job.Job + def piece: String def withJob(other: Job.Job): Piece diff --git a/src/main/scala/me/arcanis/ffxivbis/models/PieceType.scala b/src/main/scala/me/arcanis/ffxivbis/models/PieceType.scala index 88cf4b4..bb1f7ef 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/PieceType.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/PieceType.scala @@ -1,3 +1,11 @@ +/* + * Copyright (c) 2019-2022 Evgeniy Alekseev. + * + * This file is part of ffxivbis + * (see https://github.com/arcan1s/ffxivbis). + * + * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause + */ package me.arcanis.ffxivbis.models object PieceType { diff --git a/src/main/scala/me/arcanis/ffxivbis/models/Player.scala b/src/main/scala/me/arcanis/ffxivbis/models/Player.scala index 9821da1..6faeb31 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/Player.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/Player.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -21,10 +21,12 @@ case class Player( require(job ne Job.AnyJob, "AnyJob is not allowed") val playerId: PlayerId = PlayerId(partyId, job, nick) + def withBiS(set: Option[BiS]): Player = set match { case Some(value) => copy(bis = value) case None => this } + def withCounters(piece: Option[Piece]): PlayerIdWithCounters = PlayerIdWithCounters( partyId, @@ -32,12 +34,14 @@ case class Player( nick, isRequired(piece), priority, - bisCountTotal(piece), + bisCountTotal, lootCount(piece), - lootCountBiS(piece), - lootCountTotal(piece) + lootCountBiS, + lootCountTotal ) + def withLoot(piece: Loot): Player = withLoot(Seq(piece)) + def withLoot(list: Seq[Loot]): Player = { require(loot.forall(_.playerId == id), "player id must be same") copy(loot = loot ++ list) @@ -51,12 +55,16 @@ case class Player( case Some(_) => lootCount(piece) == 0 } - def bisCountTotal(piece: Option[Piece]): Int = bis.pieces.count(_.pieceType == PieceType.Savage) + def bisCountTotal: Int = bis.pieces.count(_.pieceType == PieceType.Savage) + def lootCount(piece: Option[Piece]): Int = piece match { case Some(p) => loot.count(item => !item.isFreeLoot && item.piece == p) - case None => lootCountTotal(piece) + case None => lootCountTotal } - def lootCountBiS(piece: Option[Piece]): Int = loot.map(_.piece).count(bis.hasPiece) - def lootCountTotal(piece: Option[Piece]): Int = loot.count(!_.isFreeLoot) - def lootPriority(piece: Piece): Int = priority + + def lootCountBiS: Int = loot.map(_.piece).count(bis.hasPiece) + + def lootCountTotal: Int = loot.count(!_.isFreeLoot) + + def lootPriority: Int = priority } diff --git a/src/main/scala/me/arcanis/ffxivbis/models/PlayerId.scala b/src/main/scala/me/arcanis/ffxivbis/models/PlayerId.scala index 6fbdf68..d7d1821 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/PlayerId.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/PlayerId.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -14,6 +14,7 @@ import scala.util.matching.Regex trait PlayerIdBase { def job: Job.Job + def nick: String override def toString: String = s"$nick ($job)" diff --git a/src/main/scala/me/arcanis/ffxivbis/models/PlayerIdWithCounters.scala b/src/main/scala/me/arcanis/ffxivbis/models/PlayerIdWithCounters.scala index 14703bb..ba1a1a6 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/PlayerIdWithCounters.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/PlayerIdWithCounters.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -23,7 +23,9 @@ case class PlayerIdWithCounters( def gt(that: PlayerIdWithCounters, orderBy: Seq[String]): Boolean = withCounters(orderBy) > that.withCounters(orderBy) + def isRequiredToString: String = if (isRequired) "yes" else "no" + def playerId: PlayerId = PlayerId(partyId, job, nick) private val counters: Map[String, Int] = Map( @@ -42,6 +44,7 @@ case class PlayerIdWithCounters( object PlayerIdWithCounters { private case class PlayerCountersComparator(values: Int*) { + def >(that: PlayerCountersComparator): Boolean = { @scala.annotation.tailrec def compareLists(left: List[Int], right: List[Int]): Boolean = diff --git a/src/main/scala/me/arcanis/ffxivbis/models/User.scala b/src/main/scala/me/arcanis/ffxivbis/models/User.scala index 759b5e2..17caacc 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/User.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/User.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -17,7 +17,10 @@ object Permission extends Enumeration { case class User(partyId: String, username: String, password: String, permission: Permission.Value) { def hash: String = BCrypt.hashpw(password, BCrypt.gensalt) + def verify(plain: String): Boolean = BCrypt.checkpw(plain, password) + def verityScope(scope: Permission.Value): Boolean = permission >= scope + def withHashedPassword: User = copy(password = hash) } diff --git a/src/main/scala/me/arcanis/ffxivbis/service/LootSelector.scala b/src/main/scala/me/arcanis/ffxivbis/service/LootSelector.scala index e77be60..1943af8 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/LootSelector.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/LootSelector.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). diff --git a/src/main/scala/me/arcanis/ffxivbis/service/PartyService.scala b/src/main/scala/me/arcanis/ffxivbis/service/PartyService.scala index dbea45a..b11a43e 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/PartyService.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/PartyService.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -9,11 +9,11 @@ package me.arcanis.ffxivbis.service import akka.actor.typed.scaladsl.AskPattern.Askable -import akka.actor.typed.{ActorRef, Behavior, DispatcherSelector, Scheduler} import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors} +import akka.actor.typed.{ActorRef, Behavior, DispatcherSelector, Scheduler} import akka.util.Timeout import com.typesafe.scalalogging.StrictLogging -import me.arcanis.ffxivbis.messages.{DatabaseMessage, Exists, ForgetParty, GetNewPartyId, GetParty, Message, StoreParty} +import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.models.Party import scala.concurrent.duration.FiniteDuration diff --git a/src/main/scala/me/arcanis/ffxivbis/service/bis/BisProvider.scala b/src/main/scala/me/arcanis/ffxivbis/service/bis/BisProvider.scala index edcfb55..d3e108e 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/bis/BisProvider.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/bis/BisProvider.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -8,10 +8,9 @@ */ package me.arcanis.ffxivbis.service.bis -import java.nio.file.Paths import akka.actor.ClassicActorSystemProvider -import akka.actor.typed.{Behavior, PostStop, Signal} import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors} +import akka.actor.typed.{Behavior, PostStop, Signal} import akka.http.scaladsl.model._ import com.typesafe.scalalogging.StrictLogging import me.arcanis.ffxivbis.messages.{BiSProviderMessage, DownloadBiS} @@ -20,6 +19,7 @@ import me.arcanis.ffxivbis.service.bis.parser.Parser import me.arcanis.ffxivbis.service.bis.parser.impl.{Ariyala, Etro} import spray.json._ +import java.nio.file.Paths import scala.concurrent.{ExecutionContext, Future} import scala.util.{Failure, Success} diff --git a/src/main/scala/me/arcanis/ffxivbis/service/bis/RequestExecutor.scala b/src/main/scala/me/arcanis/ffxivbis/service/bis/RequestExecutor.scala index d37dd35..4e224db 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/bis/RequestExecutor.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/bis/RequestExecutor.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). diff --git a/src/main/scala/me/arcanis/ffxivbis/service/bis/XivApi.scala b/src/main/scala/me/arcanis/ffxivbis/service/bis/XivApi.scala index bf8af9c..2db60cd 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/bis/XivApi.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/bis/XivApi.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). diff --git a/src/main/scala/me/arcanis/ffxivbis/service/bis/parser/Parser.scala b/src/main/scala/me/arcanis/ffxivbis/service/bis/parser/Parser.scala index 3d2b192..fcf4481 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/bis/parser/Parser.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/bis/parser/Parser.scala @@ -1,3 +1,11 @@ +/* + * Copyright (c) 2019-2022 Evgeniy Alekseev. + * + * This file is part of ffxivbis + * (see https://github.com/arcan1s/ffxivbis). + * + * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause + */ package me.arcanis.ffxivbis.service.bis.parser import akka.http.scaladsl.model.Uri diff --git a/src/main/scala/me/arcanis/ffxivbis/service/bis/parser/impl/Ariyala.scala b/src/main/scala/me/arcanis/ffxivbis/service/bis/parser/impl/Ariyala.scala index bbe2cda..eefae33 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/bis/parser/impl/Ariyala.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/bis/parser/impl/Ariyala.scala @@ -1,3 +1,11 @@ +/* + * Copyright (c) 2019-2022 Evgeniy Alekseev. + * + * This file is part of ffxivbis + * (see https://github.com/arcan1s/ffxivbis). + * + * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause + */ package me.arcanis.ffxivbis.service.bis.parser.impl import akka.http.scaladsl.model.Uri diff --git a/src/main/scala/me/arcanis/ffxivbis/service/bis/parser/impl/Etro.scala b/src/main/scala/me/arcanis/ffxivbis/service/bis/parser/impl/Etro.scala index 304a879..e4f357d 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/bis/parser/impl/Etro.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/bis/parser/impl/Etro.scala @@ -1,3 +1,11 @@ +/* + * Copyright (c) 2019-2022 Evgeniy Alekseev. + * + * This file is part of ffxivbis + * (see https://github.com/arcan1s/ffxivbis). + * + * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause + */ package me.arcanis.ffxivbis.service.bis.parser.impl import akka.http.scaladsl.model.Uri diff --git a/src/main/scala/me/arcanis/ffxivbis/service/database/Database.scala b/src/main/scala/me/arcanis/ffxivbis/service/database/Database.scala index 89d81ba..45969cc 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/database/Database.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/database/Database.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). diff --git a/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseBiSHandler.scala b/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseBiSHandler.scala index fc37c51..ba12d3a 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseBiSHandler.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseBiSHandler.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -9,7 +9,7 @@ package me.arcanis.ffxivbis.service.database.impl import akka.actor.typed.scaladsl.Behaviors -import me.arcanis.ffxivbis.messages.{AddPieceToBis, DatabaseMessage, GetBiS, RemovePieceFromBiS, RemovePiecesFromBiS} +import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.service.database.Database trait DatabaseBiSHandler { this: Database => diff --git a/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseImpl.scala b/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseImpl.scala index 8d006c5..5fdefeb 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseImpl.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseImpl.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -8,8 +8,8 @@ */ package me.arcanis.ffxivbis.service.database.impl -import akka.actor.typed.{Behavior, DispatcherSelector} import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext} +import akka.actor.typed.{Behavior, DispatcherSelector} import com.typesafe.config.Config import me.arcanis.ffxivbis.messages.DatabaseMessage import me.arcanis.ffxivbis.service.database.Database diff --git a/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseLootHandler.scala b/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseLootHandler.scala index 755b21d..3ae0b98 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseLootHandler.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseLootHandler.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -8,12 +8,13 @@ */ package me.arcanis.ffxivbis.service.database.impl -import java.time.Instant import akka.actor.typed.scaladsl.Behaviors -import me.arcanis.ffxivbis.messages.{AddPieceTo, DatabaseMessage, GetLoot, RemovePieceFrom, SuggestLoot} +import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.models.Loot import me.arcanis.ffxivbis.service.database.Database +import java.time.Instant + trait DatabaseLootHandler { this: Database => def lootHandler: DatabaseMessage.Handler = { 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 49b147f..fbbef7a 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -9,7 +9,7 @@ package me.arcanis.ffxivbis.service.database.impl import akka.actor.typed.scaladsl.Behaviors -import me.arcanis.ffxivbis.messages.{AddPlayer, DatabaseMessage, GetParty, GetPartyDescription, GetPlayer, RemovePlayer, UpdateParty} +import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.models.{BiS, Player} import me.arcanis.ffxivbis.service.database.Database diff --git a/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseUserHandler.scala b/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseUserHandler.scala index 5131034..d7a2a08 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseUserHandler.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseUserHandler.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -9,7 +9,7 @@ package me.arcanis.ffxivbis.service.database.impl import akka.actor.typed.scaladsl.Behaviors -import me.arcanis.ffxivbis.messages.{AddUser, DatabaseMessage, DeleteUser, Exists, GetUser, GetUsers} +import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.service.database.Database trait DatabaseUserHandler { this: Database => diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala index e8f3e05..ffbfa4b 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -8,17 +8,17 @@ */ package me.arcanis.ffxivbis.storage -import java.time.Instant - import me.arcanis.ffxivbis.models.{Job, Loot, Piece, PieceType} import slick.lifted.ForeignKeyQuery +import java.time.Instant import scala.concurrent.Future trait BiSProfile { this: DatabaseProfile => import dbConfig.profile.api._ case class BiSRep(playerId: Long, created: Long, piece: String, pieceType: String, job: String) { + def toLoot: Loot = Loot( playerId, Piece(piece, PieceType.withName(pieceType), Job.withName(job)), @@ -26,7 +26,9 @@ trait BiSProfile { this: DatabaseProfile => isFreeLoot = false ) } + object BiSRep { + def fromPiece(playerId: Long, piece: Piece): BiSRep = BiSRep(playerId, DatabaseProfile.now, piece.piece, piece.pieceType.toString, piece.job.toString) } @@ -47,11 +49,15 @@ trait BiSProfile { this: DatabaseProfile => def deletePieceBiSById(piece: Piece)(playerId: Long): Future[Int] = db.run(pieceBiS(BiSRep.fromPiece(playerId, piece)).delete) + def deletePiecesBiSById(playerId: Long): Future[Int] = db.run(piecesBiS(Seq(playerId)).delete) + def getPiecesBiSById(playerId: Long): Future[Seq[Loot]] = getPiecesBiSById(Seq(playerId)) + def getPiecesBiSById(playerIds: Seq[Long]): Future[Seq[Loot]] = db.run(piecesBiS(playerIds).result).map(_.map(_.toLoot)) + def insertPieceBiSById(piece: Piece)(playerId: Long): Future[Int] = getPiecesBiSById(playerId).flatMap { case pieces if pieces.exists(loot => loot.piece.strictEqual(piece)) => Future.successful(0) diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/DatabaseProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/DatabaseProfile.scala index d287125..2ee01cc 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/DatabaseProfile.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/DatabaseProfile.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -8,13 +8,12 @@ */ package me.arcanis.ffxivbis.storage -import java.time.Instant - import com.typesafe.config.Config import me.arcanis.ffxivbis.models.{Loot, Piece, PlayerId} import slick.basic.DatabaseConfig import slick.jdbc.JdbcProfile +import java.time.Instant import scala.concurrent.{ExecutionContext, Future} class DatabaseProfile(context: ExecutionContext, config: Config) @@ -40,12 +39,16 @@ class DatabaseProfile(context: ExecutionContext, config: Config) // generic bis api def deletePieceBiS(playerId: PlayerId, piece: Piece): Future[Int] = byPlayerId(playerId, deletePieceBiSById(piece)) + def deletePiecesBiS(playerId: PlayerId): Future[Int] = byPlayerId(playerId, deletePiecesBiSById) + def getPiecesBiS(playerId: PlayerId): Future[Seq[Loot]] = byPlayerId(playerId, getPiecesBiSById) + def getPiecesBiS(partyId: String): Future[Seq[Loot]] = byPartyId(partyId, getPiecesBiSById) + def insertPieceBiS(playerId: PlayerId, piece: Piece): Future[Int] = byPlayerId(playerId, insertPieceBiSById(piece)) @@ -55,15 +58,19 @@ class DatabaseProfile(context: ExecutionContext, config: Config) val loot = Loot(-1, piece, Instant.now, isFreeLoot = false) byPlayerId(playerId, deletePieceById(loot)) } + def getPieces(playerId: PlayerId): Future[Seq[Loot]] = byPlayerId(playerId, getPiecesById) + def getPieces(partyId: String): Future[Seq[Loot]] = byPartyId(partyId, getPiecesById) + def insertPiece(playerId: PlayerId, loot: Loot): Future[Int] = byPlayerId(playerId, insertPieceById(loot)) private def byPartyId[T](partyId: String, callback: Seq[Long] => Future[T]): Future[T] = getPlayers(partyId).flatMap(callback) + private def byPlayerId[T](playerId: PlayerId, callback: Long => Future[T]): Future[T] = getPlayer(playerId).flatMap { case Some(id) => callback(id) @@ -74,6 +81,7 @@ class DatabaseProfile(context: ExecutionContext, config: Config) object DatabaseProfile { def now: Long = Instant.now.toEpochMilli + def getSection(config: Config): Config = { val section = config.getString("me.arcanis.ffxivbis.database.mode") config.getConfig("me.arcanis.ffxivbis.database").getConfig(section) diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/LootProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/LootProfile.scala index 32c3165..5b7d775 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/LootProfile.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/LootProfile.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -8,11 +8,10 @@ */ package me.arcanis.ffxivbis.storage -import java.time.Instant - import me.arcanis.ffxivbis.models.{Job, Loot, Piece, PieceType} import slick.lifted.{ForeignKeyQuery, Index} +import java.time.Instant import scala.concurrent.Future trait LootProfile { this: DatabaseProfile => @@ -27,6 +26,7 @@ trait LootProfile { this: DatabaseProfile => job: String, isFreeLoot: Int ) { + def toLoot: Loot = Loot( playerId, Piece(piece, PieceType.withName(pieceType), Job.withName(job)), @@ -34,6 +34,7 @@ trait LootProfile { this: DatabaseProfile => isFreeLoot == 1 ) } + object LootRep { def fromLoot(playerId: Long, loot: Loot): LootRep = LootRep( @@ -70,14 +71,18 @@ trait LootProfile { this: DatabaseProfile => case Some(id) => db.run(lootTable.filter(_.lootId === id).delete) case _ => throw new IllegalArgumentException(s"Could not find piece $loot belong to $playerId") } + def getPiecesById(playerId: Long): Future[Seq[Loot]] = getPiecesById(Seq(playerId)) + def getPiecesById(playerIds: Seq[Long]): Future[Seq[Loot]] = db.run(piecesLoot(playerIds).result).map(_.map(_.toLoot)) + def insertPieceById(loot: Loot)(playerId: Long): Future[Int] = db.run(lootTable.insertOrUpdate(LootRep.fromLoot(playerId, loot))) private def pieceLoot(piece: LootRep) = piecesLoot(Seq(piece.playerId)).filter(_.piece === piece.piece) + private def piecesLoot(playerIds: Seq[Long]) = lootTable.filter(_.playerId.inSet(playerIds.toSet)) } diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/Migration.scala b/src/main/scala/me/arcanis/ffxivbis/storage/Migration.scala index c2ab08e..8c2b782 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/Migration.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/Migration.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -21,8 +21,8 @@ class Migration(config: Config) { val section = DatabaseProfile.getSection(config) val url = section.getString("db.url") - val username = section.getString("db.user") - val password = section.getString("db.password") + val username = Try(section.getString("db.user")).toOption.filter(_.nonEmpty).orNull + val password = Try(section.getString("db.password")).toOption.filter(_.nonEmpty).orNull val provider = url match { case s"jdbc:$p:$_" => p diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/PartyProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/PartyProfile.scala index d79bf48..7c8ae75 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/PartyProfile.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/PartyProfile.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -16,9 +16,12 @@ trait PartyProfile { this: DatabaseProfile => import dbConfig.profile.api._ case class PartyRep(partyId: Option[Long], partyName: String, partyAlias: Option[String]) { + def toDescription: PartyDescription = PartyDescription(partyName, partyAlias) } + object PartyRep { + def fromDescription(party: PartyDescription, id: Option[Long]): PartyRep = PartyRep(id, party.partyId, party.partyAlias) } @@ -36,8 +39,10 @@ trait PartyProfile { this: DatabaseProfile => db.run( partyDescription(partyId).result.headOption.map(_.map(_.toDescription).getOrElse(PartyDescription.empty(partyId))) ) + def getUniquePartyId(partyId: String): Future[Option[Long]] = db.run(partyDescription(partyId).map(_.partyId).result.headOption) + def insertPartyDescription(partyDescription: PartyDescription): Future[Int] = getUniquePartyId(partyDescription.partyId).flatMap { case Some(id) => db.run(partiesTable.update(PartyRep.fromDescription(partyDescription, Some(id)))) diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/PlayersProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/PlayersProfile.scala index 607a3f3..a660662 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/PlayersProfile.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/PlayersProfile.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -24,10 +24,13 @@ trait PlayersProfile { this: DatabaseProfile => link: Option[String], priority: Int ) { + def toPlayer: Player = Player(playerId.getOrElse(-1), partyId, Job.withName(job), nick, BiS.empty, Seq.empty, link, priority) } + object PlayerRep { + def fromPlayer(player: Player, id: Option[Long]): PlayerRep = PlayerRep(player.partyId, id, DatabaseProfile.now, player.nick, player.job.toString, player.link, player.priority) } @@ -46,18 +49,23 @@ trait PlayersProfile { this: DatabaseProfile => } def deletePlayer(playerId: PlayerId): Future[Int] = db.run(player(playerId).delete) + def getParty(partyId: String): Future[Map[Long, Player]] = db.run(players(partyId).result) .map(_.foldLeft(Map.empty[Long, Player]) { case (acc, p @ PlayerRep(_, Some(id), _, _, _, _, _)) => acc + (id -> p.toPlayer) case (acc, _) => acc }) + def getPlayer(playerId: PlayerId): Future[Option[Long]] = db.run(player(playerId).map(_.playerId).result.headOption) + def getPlayerFull(playerId: PlayerId): Future[Option[Player]] = db.run(player(playerId).result.headOption.map(_.map(_.toPlayer))) + def getPlayers(partyId: String): Future[Seq[Long]] = db.run(players(partyId).map(_.playerId).result) + def insertPlayer(playerObj: Player): Future[Int] = getPlayer(playerObj.playerId).flatMap { case Some(id) => db.run(playersTable.update(PlayerRep.fromPlayer(playerObj, Some(id)))) @@ -69,6 +77,7 @@ trait PlayersProfile { this: DatabaseProfile => .filter(_.partyId === playerId.partyId) .filter(_.job === playerId.job.toString) .filter(_.nick === playerId.nick) + private def players(partyId: String) = playersTable.filter(_.partyId === partyId) } diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/UsersProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/UsersProfile.scala index 7a5a09a..788775d 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/UsersProfile.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/UsersProfile.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -17,9 +17,12 @@ trait UsersProfile { this: DatabaseProfile => import dbConfig.profile.api._ case class UserRep(partyId: String, userId: Option[Long], username: String, password: String, permission: String) { + def toUser: User = User(partyId, username, password, Permission.withName(permission)) } + object UserRep { + def fromUser(user: User, id: Option[Long]): UserRep = UserRep(user.partyId, id, user.username, user.password, user.permission.toString) } @@ -40,13 +43,21 @@ trait UsersProfile { this: DatabaseProfile => } def deleteUser(partyId: String, username: String): Future[Int] = - db.run(user(partyId, Some(username)).delete) + db.run( + user(partyId, Some(username)) + .filter(_.permission =!= Permission.admin.toString) // we do not allow to remove admins + .delete + ) + def exists(partyId: String): Future[Boolean] = db.run(user(partyId, None).exists.result) + def getUser(partyId: String, username: String): Future[Option[User]] = db.run(user(partyId, Some(username)).result.headOption).map(_.map(_.toUser)) + def getUsers(partyId: String): Future[Seq[User]] = db.run(user(partyId, None).result).map(_.map(_.toUser)) + def insertUser(userObj: User): Future[Int] = db.run(user(userObj.partyId, Some(userObj.username)).map(_.userId).result.headOption).flatMap { case Some(id) => db.run(usersTable.insertOrUpdate(UserRep.fromUser(userObj, Some(id)))) diff --git a/src/main/scala/me/arcanis/ffxivbis/utils/Implicits.scala b/src/main/scala/me/arcanis/ffxivbis/utils/Implicits.scala index 0c01e61..e79b6ef 100644 --- a/src/main/scala/me/arcanis/ffxivbis/utils/Implicits.scala +++ b/src/main/scala/me/arcanis/ffxivbis/utils/Implicits.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -8,11 +8,10 @@ */ package me.arcanis.ffxivbis.utils -import java.time.Duration -import java.util.concurrent.TimeUnit - import akka.util.Timeout +import java.time.Duration +import java.util.concurrent.TimeUnit import scala.concurrent.duration.FiniteDuration import scala.language.implicitConversions diff --git a/src/test/scala/me/arcanis/ffxivbis/Fixtures.scala b/src/test/scala/me/arcanis/ffxivbis/Fixtures.scala index 042e793..0ae0cad 100644 --- a/src/test/scala/me/arcanis/ffxivbis/Fixtures.scala +++ b/src/test/scala/me/arcanis/ffxivbis/Fixtures.scala @@ -1,7 +1,10 @@ package me.arcanis.ffxivbis +import me.arcanis.ffxivbis.http.AuthorizationProvider import me.arcanis.ffxivbis.models._ +import scala.concurrent.Future + object Fixtures { lazy val bis: BiS = BiS( Seq( @@ -79,4 +82,6 @@ object Fixtures { lazy val userAdmin: User = User(partyId, "admin", userPassword, Permission.admin).withHashedPassword lazy val userGet: User = User(partyId, "get", userPassword, Permission.get).withHashedPassword lazy val users: Seq[User] = Seq(userAdmin, userGet) + + lazy val authProvider: AuthorizationProvider = (_: String, _: String) => Future.successful(Some(userAdmin)) } diff --git a/src/test/scala/me/arcanis/ffxivbis/Settings.scala b/src/test/scala/me/arcanis/ffxivbis/Settings.scala index e5c67f6..cea846e 100644 --- a/src/test/scala/me/arcanis/ffxivbis/Settings.scala +++ b/src/test/scala/me/arcanis/ffxivbis/Settings.scala @@ -1,9 +1,9 @@ package me.arcanis.ffxivbis -import java.io.File - import com.typesafe.config.{Config, ConfigFactory, ConfigValueFactory} +import java.io.File + object Settings { def config(values: Map[String, AnyRef]): Config = { @scala.annotation.tailrec diff --git a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpointTest.scala b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpointTest.scala index 9ca5a3b..2238443 100644 --- a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpointTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpointTest.scala @@ -2,20 +2,20 @@ package me.arcanis.ffxivbis.http.api.v1 import akka.actor.testkit.typed.scaladsl.ActorTestKit import akka.actor.typed.scaladsl.AskPattern.Askable -import akka.http.scaladsl.model.{StatusCodes, Uri} import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials} +import akka.http.scaladsl.model.{StatusCodes, Uri} import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest} import akka.testkit.TestKit import com.typesafe.config.Config -import me.arcanis.ffxivbis.{Fixtures, Settings} import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser} import me.arcanis.ffxivbis.models.{BiS, Job} +import me.arcanis.ffxivbis.service.PartyService import me.arcanis.ffxivbis.service.bis.BisProvider import me.arcanis.ffxivbis.service.database.Database -import me.arcanis.ffxivbis.service.PartyService import me.arcanis.ffxivbis.storage.Migration import me.arcanis.ffxivbis.utils.Compare +import me.arcanis.ffxivbis.{Fixtures, Settings} import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike @@ -31,14 +31,14 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT private val auth = Authorization(BasicHttpCredentials(Fixtures.userAdmin.username, Fixtures.userPassword)) private val endpoint = Uri(s"/party/${Fixtures.partyId}/bis") - private val playerId = PlayerIdResponse.fromPlayerId(Fixtures.playerEmpty.playerId) + private val playerId = PlayerIdModel.fromPlayerId(Fixtures.playerEmpty.playerId) private val askTimeout = 60 seconds implicit private val routeTimeout: RouteTestTimeout = RouteTestTimeout(askTimeout) private val storage = testKit.spawn(Database()) private val provider = testKit.spawn(BisProvider()) private val party = testKit.spawn(PartyService(storage)) - private val route = new BiSEndpoint(party, provider)(askTimeout, testKit.scheduler).route + private val route = new BiSEndpoint(party, provider, Fixtures.authProvider)(askTimeout, testKit.scheduler).route override def beforeAll(): Unit = { super.beforeAll() @@ -54,7 +54,7 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT super.afterAll() } - private def compareBiSResponse(actual: PlayerResponse, expected: PlayerResponse): Unit = { + private def compareBiSResponse(actual: PlayerModel, expected: PlayerModel): Unit = { actual.partyId shouldEqual expected.partyId actual.nick shouldEqual expected.nick actual.job shouldEqual expected.job @@ -66,7 +66,7 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT "api v1 bis endpoint" must { "create best in slot set from ariyala" in { - val entity = PlayerBiSLinkResponse(Fixtures.link, playerId) + val entity = PlayerBiSLinkModel(Fixtures.link, playerId) Put(endpoint, entity).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.Created @@ -76,19 +76,19 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT "return best in slot set" in { val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job))) - val response = PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis))) + val response = PlayerModel.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis))) Get(uri).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - val actual = responseAs[Seq[PlayerResponse]] + val actual = responseAs[Seq[PlayerModel]] actual.length shouldEqual 1 actual.foreach(compareBiSResponse(_, response)) } } "remove item from best in slot set" in { - val piece = PieceResponse.fromPiece(Fixtures.lootBody) - val entity = PieceActionResponse(ApiAction.remove, piece, playerId, None) + val piece = PieceModel.fromPiece(Fixtures.lootBody) + val entity = PieceActionModel(ApiAction.remove, piece, playerId, None) Post(endpoint, entity).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.Accepted @@ -97,19 +97,19 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job))) val bis = BiS(Fixtures.bis.pieces.filterNot(_ == Fixtures.lootBody)) - val response = PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(bis))) + val response = PlayerModel.fromPlayer(Fixtures.playerEmpty.withBiS(Some(bis))) Get(uri).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - val actual = responseAs[Seq[PlayerResponse]] + val actual = responseAs[Seq[PlayerModel]] actual.length shouldEqual 1 actual.foreach(compareBiSResponse(_, response)) } } "add item to best in slot set" in { - val piece = PieceResponse.fromPiece(Fixtures.lootBody) - val entity = PieceActionResponse(ApiAction.add, piece, playerId, None) + val piece = PieceModel.fromPiece(Fixtures.lootBody) + val entity = PieceActionModel(ApiAction.add, piece, playerId, None) Post(endpoint, entity).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.Accepted @@ -117,19 +117,19 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT } val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job))) - val response = PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis))) + val response = PlayerModel.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis))) Get(uri).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - val actual = responseAs[Seq[PlayerResponse]] + val actual = responseAs[Seq[PlayerModel]] actual.length shouldEqual 1 actual.foreach(compareBiSResponse(_, response)) } } "do not allow to add same item to best in slot set" in { - val piece = PieceResponse.fromPiece(Fixtures.lootBody.withJob(Job.DNC)) - val entity = PieceActionResponse(ApiAction.add, piece, playerId, None) + val piece = PieceModel.fromPiece(Fixtures.lootBody.withJob(Job.DNC)) + val entity = PieceActionModel(ApiAction.add, piece, playerId, None) Post(endpoint, entity).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.Accepted @@ -137,19 +137,19 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT } val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job))) - val response = PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis))) + val response = PlayerModel.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis))) Get(uri).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - val actual = responseAs[Seq[PlayerResponse]] + val actual = responseAs[Seq[PlayerModel]] actual.length shouldEqual 1 actual.foreach(compareBiSResponse(_, response)) } } "allow to add item with another type to best in slot set" in { - val piece = PieceResponse.fromPiece(Fixtures.lootBodyCrafted.withJob(Job.DNC)) - val entity = PieceActionResponse(ApiAction.add, piece, playerId, None) + val piece = PieceModel.fromPiece(Fixtures.lootBodyCrafted.withJob(Job.DNC)) + val entity = PieceActionModel(ApiAction.add, piece, playerId, None) Post(endpoint, entity).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.Accepted @@ -158,19 +158,19 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job))) val bis = Fixtures.bis.withPiece(piece.toPiece) - val response = PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(bis))) + val response = PlayerModel.fromPlayer(Fixtures.playerEmpty.withBiS(Some(bis))) Get(uri).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - val actual = responseAs[Seq[PlayerResponse]] + val actual = responseAs[Seq[PlayerModel]] actual.length shouldEqual 1 actual.foreach(compareBiSResponse(_, response)) } } "remove only specific item from best in slot set" in { - val piece = PieceResponse.fromPiece(Fixtures.lootBodyCrafted.withJob(Job.DNC)) - val entity = PieceActionResponse(ApiAction.remove, piece, playerId, None) + val piece = PieceModel.fromPiece(Fixtures.lootBodyCrafted.withJob(Job.DNC)) + val entity = PieceActionModel(ApiAction.remove, piece, playerId, None) Post(endpoint, entity).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.Accepted @@ -178,11 +178,11 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT } val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job))) - val response = PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis))) + val response = PlayerModel.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis))) Get(uri).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - val actual = responseAs[Seq[PlayerResponse]] + val actual = responseAs[Seq[PlayerModel]] actual.length shouldEqual 1 actual.foreach(compareBiSResponse(_, response)) } @@ -190,15 +190,15 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT "totaly replace player bis" in { // add random item first - val piece = PieceResponse.fromPiece(Fixtures.lootBodyCrafted.withJob(Job.DNC)) - val entity = PieceActionResponse(ApiAction.add, piece, playerId, None) + val piece = PieceModel.fromPiece(Fixtures.lootBodyCrafted.withJob(Job.DNC)) + val entity = PieceActionModel(ApiAction.add, piece, playerId, None) Post(endpoint, entity).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.Accepted responseAs[String] shouldEqual "" } - val bisEntity = PlayerBiSLinkResponse(Fixtures.link, playerId) + val bisEntity = PlayerBiSLinkModel(Fixtures.link, playerId) Put(endpoint, bisEntity).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.Created @@ -206,11 +206,11 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT } val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job))) - val response = PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis))) + val response = PlayerModel.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis))) Get(uri).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - val actual = responseAs[Seq[PlayerResponse]] + val actual = responseAs[Seq[PlayerModel]] actual.length shouldEqual 1 actual.foreach(compareBiSResponse(_, response)) } diff --git a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpointTest.scala b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpointTest.scala index 8a9e935..e45bf35 100644 --- a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpointTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpointTest.scala @@ -1,23 +1,22 @@ package me.arcanis.ffxivbis.http.api.v1 -import java.time.Instant - import akka.actor.testkit.typed.scaladsl.ActorTestKit import akka.actor.typed.scaladsl.AskPattern.Askable -import akka.http.scaladsl.model.{StatusCodes, Uri} import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials} +import akka.http.scaladsl.model.{StatusCodes, Uri} import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest} import akka.testkit.TestKit import com.typesafe.config.Config -import me.arcanis.ffxivbis.{Fixtures, Settings} import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser} -import me.arcanis.ffxivbis.service.database.Database import me.arcanis.ffxivbis.service.PartyService +import me.arcanis.ffxivbis.service.database.Database import me.arcanis.ffxivbis.storage.Migration +import me.arcanis.ffxivbis.{Fixtures, Settings} import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike +import java.time.Instant import scala.concurrent.Await import scala.concurrent.duration._ import scala.language.postfixOps @@ -30,13 +29,13 @@ class LootEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute private val auth = Authorization(BasicHttpCredentials(Fixtures.userAdmin.username, Fixtures.userPassword)) private val endpoint = Uri(s"/party/${Fixtures.partyId}/loot") - private val playerId = PlayerIdResponse.fromPlayerId(Fixtures.playerEmpty.playerId) + private val playerId = PlayerIdModel.fromPlayerId(Fixtures.playerEmpty.playerId) private val askTimeout = 60 seconds implicit private val routeTimeout: RouteTestTimeout = RouteTestTimeout(askTimeout) private val storage = testKit.spawn(Database()) private val party = testKit.spawn(PartyService(storage)) - private val route = new LootEndpoint(party)(askTimeout, testKit.scheduler).route + private val route = new LootEndpoint(party, Fixtures.authProvider)(askTimeout, testKit.scheduler).route override def beforeAll(): Unit = { super.beforeAll() @@ -55,8 +54,8 @@ class LootEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute "api v1 loot endpoint" must { "add item to loot" in { - val piece = PieceResponse.fromPiece(Fixtures.lootBody) - val entity = PieceActionResponse(ApiAction.add, piece, playerId, Some(false)) + val piece = PieceModel.fromPiece(Fixtures.lootBody) + val entity = PieceActionModel(ApiAction.add, piece, playerId, Some(false)) Post(endpoint, entity).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.Accepted @@ -68,11 +67,11 @@ class LootEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute import me.arcanis.ffxivbis.utils.Converters._ val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job))) - val response = Seq(PlayerResponse.fromPlayer(Fixtures.playerEmpty.withLoot(Fixtures.lootBody))) + val response = Seq(PlayerModel.fromPlayer(Fixtures.playerEmpty.withLoot(Fixtures.lootBody))) Get(uri).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - val withEmptyTimestamp = responseAs[Seq[PlayerResponse]].map { player => + val withEmptyTimestamp = responseAs[Seq[PlayerModel]].map { player => player.copy(loot = player.loot.map(_.map(_.copy(timestamp = Instant.ofEpochMilli(0))))) } withEmptyTimestamp shouldEqual response @@ -80,8 +79,8 @@ class LootEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute } "remove item from loot" in { - val piece = PieceResponse.fromPiece(Fixtures.lootBody) - val entity = PieceActionResponse(ApiAction.remove, piece, playerId, Some(false)) + val piece = PieceModel.fromPiece(Fixtures.lootBody) + val entity = PieceActionModel(ApiAction.remove, piece, playerId, Some(false)) Post(endpoint, entity).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.Accepted @@ -89,11 +88,11 @@ class LootEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute } val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job))) - val response = Seq(PlayerResponse.fromPlayer(Fixtures.playerEmpty)) + val response = Seq(PlayerModel.fromPlayer(Fixtures.playerEmpty)) Get(uri).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - responseAs[Seq[PlayerResponse]] shouldEqual response + responseAs[Seq[PlayerModel]] shouldEqual response } } diff --git a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/PartyEndpointTest.scala b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/PartyEndpointTest.scala index 6cbb3d9..9b4aae1 100644 --- a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/PartyEndpointTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/PartyEndpointTest.scala @@ -2,19 +2,19 @@ package me.arcanis.ffxivbis.http.api.v1 import akka.actor.testkit.typed.scaladsl.ActorTestKit import akka.actor.typed.scaladsl.AskPattern.Askable -import akka.http.scaladsl.model.{StatusCodes, Uri} import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials} +import akka.http.scaladsl.model.{StatusCodes, Uri} import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest} import akka.testkit.TestKit import com.typesafe.config.Config -import me.arcanis.ffxivbis.{Fixtures, Settings} import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.messages.AddUser import me.arcanis.ffxivbis.models.PartyDescription +import me.arcanis.ffxivbis.service.PartyService import me.arcanis.ffxivbis.service.bis.BisProvider import me.arcanis.ffxivbis.service.database.Database -import me.arcanis.ffxivbis.service.PartyService import me.arcanis.ffxivbis.storage.Migration +import me.arcanis.ffxivbis.{Fixtures, Settings} import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike @@ -36,7 +36,7 @@ class PartyEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRout private val storage = testKit.spawn(Database()) private val provider = testKit.spawn(BisProvider()) private val party = testKit.spawn(PartyService(storage)) - private val route = new PartyEndpoint(party, provider)(askTimeout, testKit.scheduler).route + private val route = new PartyEndpoint(party, provider, Fixtures.authProvider)(askTimeout, testKit.scheduler).route override def beforeAll(): Unit = { super.beforeAll() @@ -56,12 +56,12 @@ class PartyEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRout "get empty party description" in { Get(endpoint).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - responseAs[PartyDescriptionResponse].toDescription shouldEqual PartyDescription.empty(Fixtures.partyId) + responseAs[PartyDescriptionModel].toDescription shouldEqual PartyDescription.empty(Fixtures.partyId) } } "update party description" in { - val entity = PartyDescriptionResponse(Fixtures.partyId, Some("random party name")) + val entity = PartyDescriptionModel(Fixtures.partyId, Some("random party name")) Post(endpoint, entity).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.Accepted @@ -69,7 +69,7 @@ class PartyEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRout Get(endpoint).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - responseAs[PartyDescriptionResponse].toDescription shouldEqual entity.toDescription + responseAs[PartyDescriptionModel].toDescription shouldEqual entity.toDescription } } diff --git a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/PlayerEndpointTest.scala b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/PlayerEndpointTest.scala index 3dd649f..cd1ccfd 100644 --- a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/PlayerEndpointTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/PlayerEndpointTest.scala @@ -2,18 +2,18 @@ package me.arcanis.ffxivbis.http.api.v1 import akka.actor.testkit.typed.scaladsl.ActorTestKit import akka.actor.typed.scaladsl.AskPattern.Askable -import akka.http.scaladsl.model.{StatusCodes, Uri} import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials} +import akka.http.scaladsl.model.{StatusCodes, Uri} import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest} import akka.testkit.TestKit import com.typesafe.config.Config -import me.arcanis.ffxivbis.{Fixtures, Settings} import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser} +import me.arcanis.ffxivbis.service.PartyService import me.arcanis.ffxivbis.service.bis.BisProvider import me.arcanis.ffxivbis.service.database.Database -import me.arcanis.ffxivbis.service.PartyService import me.arcanis.ffxivbis.storage.Migration +import me.arcanis.ffxivbis.{Fixtures, Settings} import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike @@ -35,7 +35,7 @@ class PlayerEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRou private val storage = testKit.spawn(Database()) private val provider = testKit.spawn(BisProvider()) private val party = testKit.spawn(PartyService(storage)) - private val route = new PlayerEndpoint(party, provider)(askTimeout, testKit.scheduler).route + private val route = new PlayerEndpoint(party, provider, Fixtures.authProvider)(askTimeout, testKit.scheduler).route override def beforeAll(): Unit = { super.beforeAll() @@ -54,11 +54,11 @@ class PlayerEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRou "api v1 player endpoint" must { "get users" in { - val response = Seq(PlayerResponse.fromPlayer(Fixtures.playerEmpty)) + val response = Seq(PlayerModel.fromPlayer(Fixtures.playerEmpty)) Get(endpoint).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - responseAs[Seq[PlayerResponse]] shouldEqual response + responseAs[Seq[PlayerModel]] shouldEqual response } } 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 05a1720..84520aa 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 @@ -5,7 +5,7 @@ import akka.http.scaladsl.testkit.ScalatestRouteTest import com.typesafe.config.Config import me.arcanis.ffxivbis.Settings import me.arcanis.ffxivbis.http.api.v1.json._ -import me.arcanis.ffxivbis.models.{Job, Party, Permission, Piece, PieceType} +import me.arcanis.ffxivbis.models._ import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike @@ -22,6 +22,13 @@ class TypesEndpointTest extends AnyWordSpecLike "return all available jobs" in { Get("/types/jobs") ~> route ~> check { + status shouldEqual StatusCodes.OK + responseAs[Seq[String]] shouldEqual Job.available.map(_.toString) + } + } + + "return all available jobs WITH ANY JOB ALIAS" in { + Get("/types/jobs/all") ~> route ~> check { status shouldEqual StatusCodes.OK responseAs[Seq[String]] shouldEqual Job.availableWithAnyJob.map(_.toString) } 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 a965acf..26143e4 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 @@ -1,16 +1,16 @@ package me.arcanis.ffxivbis.http.api.v1 import akka.actor.testkit.typed.scaladsl.ActorTestKit -import akka.http.scaladsl.model.{StatusCodes, Uri} import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials} +import akka.http.scaladsl.model.{StatusCodes, Uri} import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest} import akka.testkit.TestKit import com.typesafe.config.Config -import me.arcanis.ffxivbis.{Fixtures, Settings} import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.service.PartyService import me.arcanis.ffxivbis.service.database.Database import me.arcanis.ffxivbis.storage.Migration +import me.arcanis.ffxivbis.{Fixtures, Settings} import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike @@ -31,7 +31,7 @@ class UserEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute private var partyId = Fixtures.partyId private val storage = testKit.spawn(Database()) private val party = testKit.spawn(PartyService(storage)) - private val route = new UserEndpoint(party)(askTimeout, testKit.scheduler).route + private val route = new UserEndpoint(party, Fixtures.authProvider)(askTimeout, testKit.scheduler).route override def beforeAll(): Unit = { super.beforeAll() @@ -49,16 +49,16 @@ class UserEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute "create a party" in { val uri = Uri(s"/party") - val entity = UserResponse.fromUser(Fixtures.userAdmin).copy(password = Fixtures.userPassword) + val entity = UserModel.fromUser(Fixtures.userAdmin).copy(password = Fixtures.userPassword) Put(uri, entity) ~> route ~> check { status shouldEqual StatusCodes.OK - partyId = responseAs[PartyIdResponse].partyId + partyId = responseAs[PartyIdModel].partyId } } "add user" in { - val entity = UserResponse.fromUser(Fixtures.userGet).copy(partyId = partyId, password = Fixtures.userPassword2) + val entity = UserModel.fromUser(Fixtures.userGet).copy(partyId = partyId, password = Fixtures.userPassword2) Post(endpoint, entity).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.Accepted @@ -73,14 +73,24 @@ class UserEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute Get(endpoint).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - val users = responseAs[Seq[UserResponse]] + val users = responseAs[Seq[UserModel]] users.map(_.partyId).distinct shouldEqual Seq(partyId) users.map(user => user.username -> user.permission).toMap shouldEqual party } } + "get current user" in { + Get(Uri(s"${endpoint.path}/current")).withHeaders(auth) ~> route ~> check { + status shouldEqual StatusCodes.OK + + val user = responseAs[UserModel] + user.partyId shouldEqual Fixtures.partyId + user.username shouldEqual Fixtures.userAdmin.username + } + } + "remove user" in { - val entity = UserResponse.fromUser(Fixtures.userGet).copy(partyId = partyId) + val entity = UserModel.fromUser(Fixtures.userGet).copy(partyId = partyId) Delete(endpoint.toString + s"/${entity.username}").withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.Accepted @@ -92,7 +102,7 @@ class UserEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute Get(endpoint).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - val users = responseAs[Seq[UserResponse]] + val users = responseAs[Seq[UserModel]] users.map(_.partyId).distinct shouldEqual Seq(partyId) users.map(user => user.username -> user.permission).toMap shouldEqual party } diff --git a/src/test/scala/me/arcanis/ffxivbis/service/LootSelectorTest.scala b/src/test/scala/me/arcanis/ffxivbis/service/LootSelectorTest.scala index 41e1aa1..fdeca76 100644 --- a/src/test/scala/me/arcanis/ffxivbis/service/LootSelectorTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/service/LootSelectorTest.scala @@ -3,9 +3,9 @@ package me.arcanis.ffxivbis.service import akka.actor.testkit.typed.scaladsl.ActorTestKit import akka.actor.typed.scaladsl.AskPattern.Askable import me.arcanis.ffxivbis.messages.DownloadBiS -import me.arcanis.ffxivbis.{Fixtures, Settings} import me.arcanis.ffxivbis.models._ import me.arcanis.ffxivbis.service.bis.BisProvider +import me.arcanis.ffxivbis.{Fixtures, Settings} import org.scalatest.BeforeAndAfterAll import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike diff --git a/src/test/scala/me/arcanis/ffxivbis/service/bis/BisProviderTest.scala b/src/test/scala/me/arcanis/ffxivbis/service/bis/BisProviderTest.scala index 654c034..6e25354 100644 --- a/src/test/scala/me/arcanis/ffxivbis/service/bis/BisProviderTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/service/bis/BisProviderTest.scala @@ -2,8 +2,8 @@ package me.arcanis.ffxivbis.service.bis import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit import me.arcanis.ffxivbis.messages.DownloadBiS -import me.arcanis.ffxivbis.{Fixtures, Settings} import me.arcanis.ffxivbis.models._ +import me.arcanis.ffxivbis.{Fixtures, Settings} import org.scalatest.wordspec.AnyWordSpecLike import scala.concurrent.duration._ diff --git a/src/test/scala/me/arcanis/ffxivbis/utils/Converters.scala b/src/test/scala/me/arcanis/ffxivbis/utils/Converters.scala index 1747252..41ede69 100644 --- a/src/test/scala/me/arcanis/ffxivbis/utils/Converters.scala +++ b/src/test/scala/me/arcanis/ffxivbis/utils/Converters.scala @@ -1,9 +1,8 @@ package me.arcanis.ffxivbis.utils -import java.time.Instant - import me.arcanis.ffxivbis.models.{Loot, Piece} +import java.time.Instant import scala.language.implicitConversions object Converters {